diff --git a/internal/api/authz/context_mock.go b/internal/api/authz/context_mock.go index 86eacfd53..bf0051c17 100644 --- a/internal/api/authz/context_mock.go +++ b/internal/api/authz/context_mock.go @@ -5,3 +5,8 @@ import "context" func NewMockContext(orgID, userID string) context.Context { return context.WithValue(context.Background(), dataKey, CtxData{UserID: userID, OrgID: orgID}) } + +func NewMockContextWithPermissions(orgID, userID string, permissions []string) context.Context { + ctx := context.WithValue(context.Background(), dataKey, CtxData{UserID: userID, OrgID: orgID}) + return context.WithValue(ctx, requestPermissionsKey, permissions) +} diff --git a/internal/api/grpc/management/project_grant.go b/internal/api/grpc/management/project_grant.go index 8b2c4d17e..c107492b3 100644 --- a/internal/api/grpc/management/project_grant.go +++ b/internal/api/grpc/management/project_grant.go @@ -126,7 +126,7 @@ func (s *Server) ListProjectGrantMembers(ctx context.Context, req *mgmt_pb.ListP } func (s *Server) AddProjectGrantMember(ctx context.Context, req *mgmt_pb.AddProjectGrantMemberRequest) (*mgmt_pb.AddProjectGrantMemberResponse, error) { - member, err := s.command.AddProjectGrantMember(ctx, AddProjectGrantMemberRequestToDomain(req), authz.GetCtxData(ctx).OrgID) + member, err := s.command.AddProjectGrantMember(ctx, AddProjectGrantMemberRequestToDomain(req)) if err != nil { return nil, err } @@ -140,7 +140,7 @@ func (s *Server) AddProjectGrantMember(ctx context.Context, req *mgmt_pb.AddProj } func (s *Server) UpdateProjectGrantMember(ctx context.Context, req *mgmt_pb.UpdateProjectGrantMemberRequest) (*mgmt_pb.UpdateProjectGrantMemberResponse, error) { - member, err := s.command.ChangeProjectGrantMember(ctx, UpdateProjectGrantMemberRequestToDomain(req), authz.GetCtxData(ctx).OrgID) + member, err := s.command.ChangeProjectGrantMember(ctx, UpdateProjectGrantMemberRequestToDomain(req)) if err != nil { return nil, err } @@ -154,7 +154,7 @@ func (s *Server) UpdateProjectGrantMember(ctx context.Context, req *mgmt_pb.Upda } func (s *Server) RemoveProjectGrantMember(ctx context.Context, req *mgmt_pb.RemoveProjectGrantMemberRequest) (*mgmt_pb.RemoveProjectGrantMemberResponse, error) { - details, err := s.command.RemoveProjectGrantMember(ctx, req.ProjectId, req.UserId, req.GrantId, authz.GetCtxData(ctx).OrgID) + details, err := s.command.RemoveProjectGrantMember(ctx, req.ProjectId, req.UserId, req.GrantId) if err != nil { return nil, err } diff --git a/internal/command/iam_member.go b/internal/command/iam_member.go index 6021ecf9b..6811643fa 100644 --- a/internal/command/iam_member.go +++ b/internal/command/iam_member.go @@ -32,14 +32,17 @@ func (c *Commands) AddIAMMember(ctx context.Context, member *domain.Member) (*do } func (c *Commands) addIAMMember(ctx context.Context, iamAgg *eventstore.Aggregate, addedMember *IAMMemberWriteModel, member *domain.Member) (eventstore.EventPusher, error) { - if !member.IsValid() { - return nil, caos_errs.ThrowPreconditionFailed(nil, "IAM-GR34U", "Errors.IAM.MemberInvalid") + if !member.IsIAMValid() { + return nil, caos_errs.ThrowInvalidArgument(nil, "IAM-GR34U", "Errors.IAM.MemberInvalid") } if len(domain.CheckForInvalidRoles(member.Roles, domain.IAMRolePrefix, c.zitadelRoles)) > 0 { - return nil, caos_errs.ThrowPreconditionFailed(nil, "IAM-4m0fS", "Errors.IAM.MemberInvalid") + return nil, caos_errs.ThrowInvalidArgument(nil, "IAM-4m0fS", "Errors.IAM.MemberInvalid") } - - err := c.eventstore.FilterToQueryReducer(ctx, addedMember) + err := c.checkUserExists(ctx, addedMember.UserID, "") + if err != nil { + return nil, caos_errs.ThrowPreconditionFailed(err, "IAM-5N9vs", "Errors.User.NotFound") + } + err = c.eventstore.FilterToQueryReducer(ctx, addedMember) if err != nil { return nil, err } @@ -52,11 +55,11 @@ func (c *Commands) addIAMMember(ctx context.Context, iamAgg *eventstore.Aggregat //ChangeIAMMember updates an existing member func (c *Commands) ChangeIAMMember(ctx context.Context, member *domain.Member) (*domain.Member, error) { - if !member.IsValid() { - return nil, caos_errs.ThrowPreconditionFailed(nil, "IAM-LiaZi", "Errors.IAM.MemberInvalid") + if !member.IsIAMValid() { + return nil, caos_errs.ThrowInvalidArgument(nil, "IAM-LiaZi", "Errors.IAM.MemberInvalid") } if len(domain.CheckForInvalidRoles(member.Roles, domain.IAMRolePrefix, c.zitadelRoles)) > 0 { - return nil, caos_errs.ThrowPreconditionFailed(nil, "IAM-3m9fs", "Errors.IAM.MemberInvalid") + return nil, caos_errs.ThrowInvalidArgument(nil, "IAM-3m9fs", "Errors.IAM.MemberInvalid") } existingMember, err := c.iamMemberWriteModelByID(ctx, member.UserID) @@ -81,6 +84,9 @@ func (c *Commands) ChangeIAMMember(ctx context.Context, member *domain.Member) ( } func (c *Commands) RemoveIAMMember(ctx context.Context, userID string) (*domain.ObjectDetails, error) { + if userID == "" { + return nil, caos_errs.ThrowInvalidArgument(nil, "IAM-LiaZi", "Errors.IDMissing") + } memberWriteModel, err := c.iamMemberWriteModelByID(ctx, userID) if err != nil && !errors.IsNotFound(err) { return nil, err diff --git a/internal/command/iam_member_test.go b/internal/command/iam_member_test.go new file mode 100644 index 000000000..240883b82 --- /dev/null +++ b/internal/command/iam_member_test.go @@ -0,0 +1,552 @@ +package command + +import ( + "context" + "github.com/caos/zitadel/internal/api/authz" + "github.com/caos/zitadel/internal/domain" + caos_errs "github.com/caos/zitadel/internal/errors" + "github.com/caos/zitadel/internal/eventstore" + "github.com/caos/zitadel/internal/eventstore/repository" + "github.com/caos/zitadel/internal/eventstore/v1/models" + "github.com/caos/zitadel/internal/repository/iam" + "github.com/caos/zitadel/internal/repository/member" + "github.com/caos/zitadel/internal/repository/user" + "github.com/stretchr/testify/assert" + "golang.org/x/text/language" + "testing" +) + +func TestCommandSide_AddIAMMember(t *testing.T) { + type fields struct { + eventstore *eventstore.Eventstore + zitadelRoles []authz.RoleMapping + } + type args struct { + ctx context.Context + member *domain.Member + } + type res struct { + want *domain.Member + err func(error) bool + } + tests := []struct { + name string + fields fields + args args + res res + }{ + { + name: "invalid member, error", + fields: fields{ + eventstore: eventstoreExpect( + t, + ), + }, + args: args{ + ctx: context.Background(), + member: &domain.Member{}, + }, + res: res{ + err: caos_errs.IsErrorInvalidArgument, + }, + }, + { + name: "invalid roles, error", + fields: fields{ + eventstore: eventstoreExpect( + t, + ), + }, + args: args{ + ctx: context.Background(), + member: &domain.Member{ + UserID: "user1", + Roles: []string{"IAM_OWNER"}, + }, + }, + res: res{ + err: caos_errs.IsErrorInvalidArgument, + }, + }, + { + name: "user not existing, precondition error", + fields: fields{ + eventstore: eventstoreExpect( + t, + expectFilter(), + ), + zitadelRoles: []authz.RoleMapping{ + { + Role: "IAM_OWNER", + }, + }, + }, + args: args{ + ctx: context.Background(), + member: &domain.Member{ + UserID: "user1", + Roles: []string{"IAM_OWNER"}, + }, + }, + res: res{ + err: caos_errs.IsPreconditionFailed, + }, + }, + { + name: "member already exists, precondition error", + fields: fields{ + eventstore: eventstoreExpect( + t, + expectFilter( + eventFromEventPusher( + user.NewHumanAddedEvent(context.Background(), + &user.NewAggregate("user1", "org1").Aggregate, + "username1", + "firstname1", + "lastname1", + "nickname1", + "displayname1", + language.German, + domain.GenderMale, + "email1", + true, + ), + ), + ), + expectFilter( + eventFromEventPusher( + iam.NewMemberAddedEvent(context.Background(), + &iam.NewAggregate().Aggregate, + "user1", + ), + ), + ), + ), + zitadelRoles: []authz.RoleMapping{ + { + Role: "IAM_OWNER", + }, + }, + }, + args: args{ + ctx: context.Background(), + member: &domain.Member{ + UserID: "user1", + Roles: []string{"IAM_OWNER"}, + }, + }, + res: res{ + err: caos_errs.IsErrorAlreadyExists, + }, + }, + { + name: "member add uniqueconstraint err, already exists", + fields: fields{ + eventstore: eventstoreExpect( + t, + expectFilter( + eventFromEventPusher( + user.NewHumanAddedEvent(context.Background(), + &user.NewAggregate("user1", "org1").Aggregate, + "username1", + "firstname1", + "lastname1", + "nickname1", + "displayname1", + language.German, + domain.GenderMale, + "email1", + true, + ), + ), + ), + expectFilter(), + expectPushFailed(caos_errs.ThrowAlreadyExists(nil, "ERROR", "internal"), + []*repository.Event{ + eventFromEventPusher(iam.NewMemberAddedEvent(context.Background(), + &iam.NewAggregate().Aggregate, + "user1", + []string{"IAM_OWNER"}..., + )), + }, + uniqueConstraintsFromEventConstraint(member.NewAddMemberUniqueConstraint("IAM", "user1")), + ), + ), + zitadelRoles: []authz.RoleMapping{ + { + Role: "IAM_OWNER", + }, + }, + }, + args: args{ + ctx: context.Background(), + member: &domain.Member{ + UserID: "user1", + Roles: []string{"IAM_OWNER"}, + }, + }, + res: res{ + err: caos_errs.IsErrorAlreadyExists, + }, + }, + { + name: "member add, ok", + fields: fields{ + eventstore: eventstoreExpect( + t, + expectFilter( + eventFromEventPusher( + user.NewHumanAddedEvent(context.Background(), + &user.NewAggregate("user1", "org1").Aggregate, + "username1", + "firstname1", + "lastname1", + "nickname1", + "displayname1", + language.German, + domain.GenderMale, + "email1", + true, + ), + ), + ), + expectFilter(), + expectPush( + []*repository.Event{ + eventFromEventPusher(iam.NewMemberAddedEvent(context.Background(), + &iam.NewAggregate().Aggregate, + "user1", + []string{"IAM_OWNER"}..., + )), + }, + uniqueConstraintsFromEventConstraint(member.NewAddMemberUniqueConstraint("IAM", "user1")), + ), + ), + zitadelRoles: []authz.RoleMapping{ + { + Role: "IAM_OWNER", + }, + }, + }, + args: args{ + ctx: context.Background(), + member: &domain.Member{ + UserID: "user1", + Roles: []string{"IAM_OWNER"}, + }, + }, + res: res{ + want: &domain.Member{ + ObjectRoot: models.ObjectRoot{ + ResourceOwner: "IAM", + AggregateID: "IAM", + }, + UserID: "user1", + Roles: []string{"IAM_OWNER"}, + }, + }, + }, + } + 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.AddIAMMember(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_ChangeIAMMember(t *testing.T) { + type fields struct { + eventstore *eventstore.Eventstore + zitadelRoles []authz.RoleMapping + } + type args struct { + ctx context.Context + member *domain.Member + } + type res struct { + want *domain.Member + err func(error) bool + } + tests := []struct { + name string + fields fields + args args + res res + }{ + { + name: "invalid member, error", + fields: fields{ + eventstore: eventstoreExpect( + t, + ), + }, + args: args{ + ctx: context.Background(), + member: &domain.Member{}, + }, + res: res{ + err: caos_errs.IsErrorInvalidArgument, + }, + }, + { + name: "invalid roles, error", + fields: fields{ + eventstore: eventstoreExpect( + t, + ), + }, + args: args{ + ctx: context.Background(), + member: &domain.Member{ + UserID: "user1", + Roles: []string{"IAM_OWNER"}, + }, + }, + res: res{ + err: caos_errs.IsErrorInvalidArgument, + }, + }, + { + name: "member not existing, not found error", + fields: fields{ + eventstore: eventstoreExpect( + t, + expectFilter(), + ), + zitadelRoles: []authz.RoleMapping{ + { + Role: "IAM_OWNER", + }, + }, + }, + args: args{ + ctx: context.Background(), + member: &domain.Member{ + UserID: "user1", + Roles: []string{"IAM_OWNER"}, + }, + }, + res: res{ + err: caos_errs.IsNotFound, + }, + }, + { + name: "member not changed, precondition error", + fields: fields{ + eventstore: eventstoreExpect( + t, + expectFilter( + eventFromEventPusher( + iam.NewMemberAddedEvent(context.Background(), + &iam.NewAggregate().Aggregate, + "user1", + []string{"IAM_OWNER"}..., + ), + ), + ), + ), + zitadelRoles: []authz.RoleMapping{ + { + Role: domain.RoleIAMOwner, + }, + }, + }, + args: args{ + ctx: context.Background(), + member: &domain.Member{ + UserID: "user1", + Roles: []string{"IAM_OWNER"}, + }, + }, + res: res{ + err: caos_errs.IsPreconditionFailed, + }, + }, + { + name: "member change, ok", + fields: fields{ + eventstore: eventstoreExpect( + t, + expectFilter( + eventFromEventPusher( + iam.NewMemberAddedEvent(context.Background(), + &iam.NewAggregate().Aggregate, + "user1", + []string{"IAM_OWNER"}..., + ), + ), + ), + expectPush( + []*repository.Event{ + eventFromEventPusher(iam.NewMemberChangedEvent(context.Background(), + &iam.NewAggregate().Aggregate, + "user1", + []string{"IAM_OWNER", "IAM_OWNER_VIEWER"}..., + )), + }, + ), + ), + zitadelRoles: []authz.RoleMapping{ + { + Role: "IAM_OWNER", + }, + { + Role: "IAM_OWNER_VIEWER", + }, + }, + }, + args: args{ + ctx: context.Background(), + member: &domain.Member{ + UserID: "user1", + Roles: []string{"IAM_OWNER", "IAM_OWNER_VIEWER"}, + }, + }, + res: res{ + want: &domain.Member{ + ObjectRoot: models.ObjectRoot{ + ResourceOwner: "IAM", + AggregateID: "IAM", + }, + UserID: "user1", + Roles: []string{"IAM_OWNER", "IAM_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.ChangeIAMMember(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_RemoveIAMMember(t *testing.T) { + type fields struct { + eventstore *eventstore.Eventstore + } + type args struct { + ctx context.Context + 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 userid missing, error", + fields: fields{ + eventstore: eventstoreExpect( + t, + ), + }, + args: args{ + ctx: context.Background(), + userID: "", + }, + res: res{ + err: caos_errs.IsErrorInvalidArgument, + }, + }, + { + name: "member not existing, nil result", + fields: fields{ + eventstore: eventstoreExpect( + t, + expectFilter(), + ), + }, + args: args{ + ctx: context.Background(), + userID: "user1", + }, + res: res{ + want: nil, + }, + }, + { + name: "member remove, ok", + fields: fields{ + eventstore: eventstoreExpect( + t, + expectFilter( + eventFromEventPusher( + iam.NewMemberAddedEvent(context.Background(), + &iam.NewAggregate().Aggregate, + "user1", + []string{"IAM_OWNER"}..., + ), + ), + ), + expectPush( + []*repository.Event{ + eventFromEventPusher(iam.NewMemberRemovedEvent(context.Background(), + &iam.NewAggregate().Aggregate, + "user1", + )), + }, + uniqueConstraintsFromEventConstraint(member.NewRemoveMemberUniqueConstraint("IAM", "user1")), + ), + ), + }, + args: args{ + ctx: context.Background(), + userID: "user1", + }, + res: res{ + want: &domain.ObjectDetails{ + ResourceOwner: "IAM", + }, + }, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + r := &Commands{ + eventstore: tt.fields.eventstore, + } + got, err := r.RemoveIAMMember(tt.args.ctx, tt.args.userID) + 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) + } + }) + } +} diff --git a/internal/command/iam_policy_label.go b/internal/command/iam_policy_label.go index 58fb21975..6a5ba5af1 100644 --- a/internal/command/iam_policy_label.go +++ b/internal/command/iam_policy_label.go @@ -29,6 +29,9 @@ func (c *Commands) AddDefaultLabelPolicy(ctx context.Context, policy *domain.Lab } func (c *Commands) addDefaultLabelPolicy(ctx context.Context, iamAgg *eventstore.Aggregate, addedPolicy *IAMLabelPolicyWriteModel, policy *domain.LabelPolicy) (eventstore.EventPusher, error) { + if !policy.IsValid() { + return nil, caos_errs.ThrowInvalidArgument(nil, "IAM-3m9fo", "Errors.IAM.LabelPolicy.Invalid") + } err := c.eventstore.FilterToQueryReducer(ctx, addedPolicy) if err != nil { return nil, err @@ -42,6 +45,9 @@ func (c *Commands) addDefaultLabelPolicy(ctx context.Context, iamAgg *eventstore } func (c *Commands) ChangeDefaultLabelPolicy(ctx context.Context, policy *domain.LabelPolicy) (*domain.LabelPolicy, error) { + if !policy.IsValid() { + return nil, caos_errs.ThrowInvalidArgument(nil, "IAM-33m8f", "Errors.IAM.LabelPolicy.Invalid") + } existingPolicy, err := c.defaultLabelPolicyWriteModelByID(ctx) if err != nil { return nil, err diff --git a/internal/command/iam_policy_label_test.go b/internal/command/iam_policy_label_test.go new file mode 100644 index 000000000..64775a2c3 --- /dev/null +++ b/internal/command/iam_policy_label_test.go @@ -0,0 +1,288 @@ +package command + +import ( + "context" + "github.com/caos/zitadel/internal/domain" + caos_errs "github.com/caos/zitadel/internal/errors" + "github.com/caos/zitadel/internal/eventstore" + "github.com/caos/zitadel/internal/eventstore/repository" + "github.com/caos/zitadel/internal/eventstore/v1/models" + "github.com/caos/zitadel/internal/repository/iam" + "github.com/caos/zitadel/internal/repository/policy" + "github.com/stretchr/testify/assert" + "testing" +) + +func TestCommandSide_AddDefaultLabelPolicy(t *testing.T) { + type fields struct { + eventstore *eventstore.Eventstore + } + type args struct { + ctx context.Context + policy *domain.LabelPolicy + } + type res struct { + want *domain.LabelPolicy + err func(error) bool + } + tests := []struct { + name string + fields fields + args args + res res + }{ + { + name: "labelpolicy invalid, invalid argument error", + fields: fields{ + eventstore: eventstoreExpect( + t, + ), + }, + args: args{ + ctx: context.Background(), + policy: &domain.LabelPolicy{ + PrimaryColor: "", + SecondaryColor: "secondary-color", + }, + }, + res: res{ + err: caos_errs.IsErrorInvalidArgument, + }, + }, + { + name: "labelpolicy already existing, already exists error", + fields: fields{ + eventstore: eventstoreExpect( + t, + expectFilter( + eventFromEventPusher( + iam.NewLabelPolicyAddedEvent(context.Background(), + &iam.NewAggregate().Aggregate, + "primary-color", + "secondary-color", + ), + ), + ), + ), + }, + args: args{ + ctx: context.Background(), + policy: &domain.LabelPolicy{ + PrimaryColor: "primary-color", + SecondaryColor: "secondary-color", + }, + }, + res: res{ + err: caos_errs.IsErrorAlreadyExists, + }, + }, + { + name: "add policy,ok", + fields: fields{ + eventstore: eventstoreExpect( + t, + expectFilter(), + expectPush( + []*repository.Event{ + eventFromEventPusher( + iam.NewLabelPolicyAddedEvent(context.Background(), + &iam.NewAggregate().Aggregate, + "primary-color", + "secondary-color", + ), + ), + }, + ), + ), + }, + args: args{ + ctx: context.Background(), + policy: &domain.LabelPolicy{ + PrimaryColor: "primary-color", + SecondaryColor: "secondary-color", + }, + }, + res: res{ + want: &domain.LabelPolicy{ + ObjectRoot: models.ObjectRoot{ + AggregateID: "IAM", + ResourceOwner: "IAM", + }, + PrimaryColor: "primary-color", + SecondaryColor: "secondary-color", + }, + }, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + r := &Commands{ + eventstore: tt.fields.eventstore, + } + got, err := r.AddDefaultLabelPolicy(tt.args.ctx, tt.args.policy) + 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_ChangeDefaultLabelPolicy(t *testing.T) { + type fields struct { + eventstore *eventstore.Eventstore + } + type args struct { + ctx context.Context + policy *domain.LabelPolicy + } + type res struct { + want *domain.LabelPolicy + err func(error) bool + } + tests := []struct { + name string + fields fields + args args + res res + }{ + { + name: "labelpolicy invalid, invalid argument error", + fields: fields{ + eventstore: eventstoreExpect( + t, + ), + }, + args: args{ + ctx: context.Background(), + policy: &domain.LabelPolicy{ + PrimaryColor: "", + SecondaryColor: "secondary-color", + }, + }, + res: res{ + err: caos_errs.IsErrorInvalidArgument, + }, + }, + { + name: "labelpolicy not existing, not found error", + fields: fields{ + eventstore: eventstoreExpect( + t, + expectFilter(), + ), + }, + args: args{ + ctx: context.Background(), + policy: &domain.LabelPolicy{ + PrimaryColor: "primary-color", + SecondaryColor: "secondary-color", + }, + }, + res: res{ + err: caos_errs.IsNotFound, + }, + }, + { + name: "no changes, precondition error", + fields: fields{ + eventstore: eventstoreExpect( + t, + expectFilter( + eventFromEventPusher( + iam.NewLabelPolicyAddedEvent(context.Background(), + &iam.NewAggregate().Aggregate, + "primary-color", + "secondary-color", + ), + ), + ), + ), + }, + args: args{ + ctx: context.Background(), + policy: &domain.LabelPolicy{ + PrimaryColor: "primary-color", + SecondaryColor: "secondary-color", + }, + }, + res: res{ + err: caos_errs.IsPreconditionFailed, + }, + }, + { + name: "change, ok", + fields: fields{ + eventstore: eventstoreExpect( + t, + expectFilter( + eventFromEventPusher( + iam.NewLabelPolicyAddedEvent(context.Background(), + &iam.NewAggregate().Aggregate, + "primary-color", + "secondary-color", + ), + ), + ), + expectPush( + []*repository.Event{ + eventFromEventPusher( + newDefaultLabelPolicyChangedEvent(context.Background(), "primary-color-change", "secondary-color-change"), + ), + }, + ), + ), + }, + args: args{ + ctx: context.Background(), + policy: &domain.LabelPolicy{ + PrimaryColor: "primary-color-change", + SecondaryColor: "secondary-color-change", + }, + }, + res: res{ + want: &domain.LabelPolicy{ + ObjectRoot: models.ObjectRoot{ + AggregateID: "IAM", + ResourceOwner: "IAM", + }, + PrimaryColor: "primary-color-change", + SecondaryColor: "secondary-color-change", + }, + }, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + r := &Commands{ + eventstore: tt.fields.eventstore, + } + got, err := r.ChangeDefaultLabelPolicy(tt.args.ctx, tt.args.policy) + 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 newDefaultLabelPolicyChangedEvent(ctx context.Context, primaryColor, secondaryColor string) *iam.LabelPolicyChangedEvent { + event, _ := iam.NewLabelPolicyChangedEvent(ctx, + &iam.NewAggregate().Aggregate, + []policy.LabelPolicyChanges{ + policy.ChangePrimaryColor(primaryColor), + policy.ChangeSecondaryColor(secondaryColor), + }, + ) + return event +} diff --git a/internal/command/iam_policy_login.go b/internal/command/iam_policy_login.go index 6f7a05740..28ff1b70a 100644 --- a/internal/command/iam_policy_login.go +++ b/internal/command/iam_policy_login.go @@ -28,11 +28,14 @@ func (c *Commands) AddDefaultLoginPolicy(ctx context.Context, policy *domain.Log if err != nil { return nil, err } - _, err = c.eventstore.PushEvents(ctx, event) + pushedEvents, err := c.eventstore.PushEvents(ctx, event) + if err != nil { + return nil, err + } + err = AppendAndReduce(addedPolicy, pushedEvents...) if err != nil { return nil, err } - return writeModelToLoginPolicy(&addedPolicy.LoginPolicyWriteModel), nil } diff --git a/internal/command/iam_policy_login_test.go b/internal/command/iam_policy_login_test.go new file mode 100644 index 000000000..b5055f86e --- /dev/null +++ b/internal/command/iam_policy_login_test.go @@ -0,0 +1,283 @@ +package command + +import ( + "context" + "github.com/caos/zitadel/internal/domain" + caos_errs "github.com/caos/zitadel/internal/errors" + "github.com/caos/zitadel/internal/eventstore" + "github.com/caos/zitadel/internal/eventstore/repository" + "github.com/caos/zitadel/internal/eventstore/v1/models" + "github.com/caos/zitadel/internal/repository/iam" + "github.com/caos/zitadel/internal/repository/policy" + "github.com/stretchr/testify/assert" + "testing" +) + +func TestCommandSide_AddDefaultLoginPolicy(t *testing.T) { + type fields struct { + eventstore *eventstore.Eventstore + } + type args struct { + ctx context.Context + policy *domain.LoginPolicy + } + type res struct { + want *domain.LoginPolicy + err func(error) bool + } + tests := []struct { + name string + fields fields + args args + res res + }{ + { + name: "loginpolicy already existing, already exists error", + fields: fields{ + eventstore: eventstoreExpect( + t, + expectFilter( + eventFromEventPusher( + iam.NewLoginPolicyAddedEvent(context.Background(), + &iam.NewAggregate().Aggregate, + true, + true, + false, + false, + domain.PasswordlessTypeAllowed, + ), + ), + ), + ), + }, + args: args{ + ctx: context.Background(), + policy: &domain.LoginPolicy{ + AllowRegister: true, + AllowUsernamePassword: true, + PasswordlessType: domain.PasswordlessTypeAllowed, + }, + }, + res: res{ + err: caos_errs.IsErrorAlreadyExists, + }, + }, + { + name: "add policy,ok", + fields: fields{ + eventstore: eventstoreExpect( + t, + expectFilter(), + expectPush( + []*repository.Event{ + eventFromEventPusher( + iam.NewLoginPolicyAddedEvent(context.Background(), + &iam.NewAggregate().Aggregate, + true, + true, + true, + true, + domain.PasswordlessTypeAllowed, + ), + ), + }, + ), + ), + }, + args: args{ + ctx: context.Background(), + policy: &domain.LoginPolicy{ + AllowRegister: true, + AllowUsernamePassword: true, + AllowExternalIDP: true, + ForceMFA: true, + PasswordlessType: domain.PasswordlessTypeAllowed, + }, + }, + res: res{ + want: &domain.LoginPolicy{ + ObjectRoot: models.ObjectRoot{ + AggregateID: "IAM", + ResourceOwner: "IAM", + }, + AllowRegister: true, + AllowUsernamePassword: true, + AllowExternalIDP: true, + ForceMFA: true, + PasswordlessType: domain.PasswordlessTypeAllowed, + }, + }, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + r := &Commands{ + eventstore: tt.fields.eventstore, + } + got, err := r.AddDefaultLoginPolicy(tt.args.ctx, tt.args.policy) + 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_ChangeDefaultLoginPolicy(t *testing.T) { + type fields struct { + eventstore *eventstore.Eventstore + } + type args struct { + ctx context.Context + policy *domain.LoginPolicy + } + type res struct { + want *domain.LoginPolicy + err func(error) bool + } + tests := []struct { + name string + fields fields + args args + res res + }{ + { + name: "loginpolicy not existing, not found error", + fields: fields{ + eventstore: eventstoreExpect( + t, + expectFilter(), + ), + }, + args: args{ + ctx: context.Background(), + policy: &domain.LoginPolicy{ + AllowRegister: true, + AllowExternalIDP: true, + }, + }, + res: res{ + err: caos_errs.IsNotFound, + }, + }, + { + name: "no changes, precondition error", + fields: fields{ + eventstore: eventstoreExpect( + t, + expectFilter( + eventFromEventPusher( + iam.NewLoginPolicyAddedEvent(context.Background(), + &iam.NewAggregate().Aggregate, + true, + true, + true, + true, + domain.PasswordlessTypeAllowed, + ), + ), + ), + ), + }, + args: args{ + ctx: context.Background(), + policy: &domain.LoginPolicy{ + AllowRegister: true, + AllowUsernamePassword: true, + AllowExternalIDP: true, + ForceMFA: true, + PasswordlessType: domain.PasswordlessTypeAllowed, + }, + }, + res: res{ + err: caos_errs.IsPreconditionFailed, + }, + }, + { + name: "change, ok", + fields: fields{ + eventstore: eventstoreExpect( + t, + expectFilter( + eventFromEventPusher( + iam.NewLoginPolicyAddedEvent(context.Background(), + &iam.NewAggregate().Aggregate, + true, + true, + true, + true, + domain.PasswordlessTypeAllowed, + ), + ), + ), + expectPush( + []*repository.Event{ + eventFromEventPusher( + newDefaultLoginPolicyChangedEvent(context.Background(), false, false, false, false, domain.PasswordlessTypeNotAllowed), + ), + }, + ), + ), + }, + args: args{ + ctx: context.Background(), + policy: &domain.LoginPolicy{ + AllowRegister: false, + AllowUsernamePassword: false, + AllowExternalIDP: false, + ForceMFA: false, + PasswordlessType: domain.PasswordlessTypeNotAllowed, + }, + }, + res: res{ + want: &domain.LoginPolicy{ + ObjectRoot: models.ObjectRoot{ + AggregateID: "IAM", + ResourceOwner: "IAM", + }, + AllowRegister: false, + AllowUsernamePassword: false, + AllowExternalIDP: false, + ForceMFA: false, + PasswordlessType: domain.PasswordlessTypeNotAllowed, + }, + }, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + r := &Commands{ + eventstore: tt.fields.eventstore, + } + got, err := r.ChangeDefaultLoginPolicy(tt.args.ctx, tt.args.policy) + 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 newDefaultLoginPolicyChangedEvent(ctx context.Context, allowRegister, allowUsernamePassword, allowExternalIDP, forceMFA bool, passwordlessType domain.PasswordlessType) *iam.LoginPolicyChangedEvent { + event, _ := iam.NewLoginPolicyChangedEvent(ctx, + &iam.NewAggregate().Aggregate, + []policy.LoginPolicyChanges{ + policy.ChangeAllowRegister(allowRegister), + policy.ChangeAllowExternalIDP(allowExternalIDP), + policy.ChangeForceMFA(forceMFA), + policy.ChangeAllowUserNamePassword(allowUsernamePassword), + policy.ChangePasswordlessType(passwordlessType), + }, + ) + return event +} diff --git a/internal/command/iam_policy_mail_template.go b/internal/command/iam_policy_mail_template.go index dcbed1a10..4604dd585 100644 --- a/internal/command/iam_policy_mail_template.go +++ b/internal/command/iam_policy_mail_template.go @@ -30,7 +30,7 @@ func (c *Commands) AddDefaultMailTemplate(ctx context.Context, policy *domain.Ma func (c *Commands) addDefaultMailTemplate(ctx context.Context, iamAgg *eventstore.Aggregate, addedPolicy *IAMMailTemplateWriteModel, policy *domain.MailTemplate) (eventstore.EventPusher, error) { if !policy.IsValid() { - return nil, caos_errs.ThrowPreconditionFailed(nil, "IAM-fm9sd", "Errors.IAM.MailTemplate.Invalid") + return nil, caos_errs.ThrowInvalidArgument(nil, "IAM-fm9sd", "Errors.IAM.MailTemplate.Invalid") } err := c.eventstore.FilterToQueryReducer(ctx, addedPolicy) if err != nil { @@ -45,7 +45,7 @@ func (c *Commands) addDefaultMailTemplate(ctx context.Context, iamAgg *eventstor func (c *Commands) ChangeDefaultMailTemplate(ctx context.Context, policy *domain.MailTemplate) (*domain.MailTemplate, error) { if !policy.IsValid() { - return nil, caos_errs.ThrowPreconditionFailed(nil, "IAM-4m9ds", "Errors.IAM.MailTemplate.Invalid") + return nil, caos_errs.ThrowInvalidArgument(nil, "IAM-4m9ds", "Errors.IAM.MailTemplate.Invalid") } existingPolicy, err := c.defaultMailTemplateWriteModelByID(ctx) if err != nil { diff --git a/internal/command/iam_policy_mail_template_test.go b/internal/command/iam_policy_mail_template_test.go new file mode 100644 index 000000000..1f03ac795 --- /dev/null +++ b/internal/command/iam_policy_mail_template_test.go @@ -0,0 +1,270 @@ +package command + +import ( + "context" + "github.com/caos/zitadel/internal/domain" + caos_errs "github.com/caos/zitadel/internal/errors" + "github.com/caos/zitadel/internal/eventstore" + "github.com/caos/zitadel/internal/eventstore/repository" + "github.com/caos/zitadel/internal/eventstore/v1/models" + "github.com/caos/zitadel/internal/repository/iam" + "github.com/caos/zitadel/internal/repository/policy" + "github.com/stretchr/testify/assert" + "testing" +) + +func TestCommandSide_AddDefaultMailTemplatePolicy(t *testing.T) { + type fields struct { + eventstore *eventstore.Eventstore + } + type args struct { + ctx context.Context + policy *domain.MailTemplate + } + type res struct { + want *domain.MailTemplate + err func(error) bool + } + tests := []struct { + name string + fields fields + args args + res res + }{ + { + name: "mailtemplate invalid, invalid argument error", + fields: fields{ + eventstore: eventstoreExpect( + t, + ), + }, + args: args{ + ctx: context.Background(), + policy: &domain.MailTemplate{}, + }, + res: res{ + err: caos_errs.IsErrorInvalidArgument, + }, + }, + { + name: "mailtemplate already existing, already exists error", + fields: fields{ + eventstore: eventstoreExpect( + t, + expectFilter( + eventFromEventPusher( + iam.NewMailTemplateAddedEvent(context.Background(), + &iam.NewAggregate().Aggregate, + []byte("template"), + ), + ), + ), + ), + }, + args: args{ + ctx: context.Background(), + policy: &domain.MailTemplate{ + Template: []byte("template"), + }, + }, + res: res{ + err: caos_errs.IsErrorAlreadyExists, + }, + }, + { + name: "add mail template,ok", + fields: fields{ + eventstore: eventstoreExpect( + t, + expectFilter(), + expectPush( + []*repository.Event{ + eventFromEventPusher( + iam.NewMailTemplateAddedEvent(context.Background(), + &iam.NewAggregate().Aggregate, + []byte("template"), + ), + ), + }, + ), + ), + }, + args: args{ + ctx: context.Background(), + policy: &domain.MailTemplate{ + Template: []byte("template"), + }, + }, + res: res{ + want: &domain.MailTemplate{ + ObjectRoot: models.ObjectRoot{ + AggregateID: "IAM", + ResourceOwner: "IAM", + }, + Template: []byte("template"), + }, + }, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + r := &Commands{ + eventstore: tt.fields.eventstore, + } + got, err := r.AddDefaultMailTemplate(tt.args.ctx, tt.args.policy) + 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_ChangeDefaultMailTemplatePolicy(t *testing.T) { + type fields struct { + eventstore *eventstore.Eventstore + } + type args struct { + ctx context.Context + policy *domain.MailTemplate + } + type res struct { + want *domain.MailTemplate + err func(error) bool + } + tests := []struct { + name string + fields fields + args args + res res + }{ + { + name: "mailtemplate invalid, invalid argument error", + fields: fields{ + eventstore: eventstoreExpect( + t, + ), + }, + args: args{ + ctx: context.Background(), + policy: &domain.MailTemplate{}, + }, + res: res{ + err: caos_errs.IsErrorInvalidArgument, + }, + }, + { + name: "mailtempalte not existing, not found error", + fields: fields{ + eventstore: eventstoreExpect( + t, + expectFilter(), + ), + }, + args: args{ + ctx: context.Background(), + policy: &domain.MailTemplate{ + Template: []byte("template-change"), + }, + }, + res: res{ + err: caos_errs.IsNotFound, + }, + }, + { + name: "no changes, precondition error", + fields: fields{ + eventstore: eventstoreExpect( + t, + expectFilter( + eventFromEventPusher( + iam.NewMailTemplateAddedEvent(context.Background(), + &iam.NewAggregate().Aggregate, + []byte("template"), + ), + ), + ), + ), + }, + args: args{ + ctx: context.Background(), + policy: &domain.MailTemplate{ + Template: []byte("template"), + }, + }, + res: res{ + err: caos_errs.IsPreconditionFailed, + }, + }, + { + name: "change, ok", + fields: fields{ + eventstore: eventstoreExpect( + t, + expectFilter( + eventFromEventPusher( + iam.NewMailTemplateAddedEvent(context.Background(), + &iam.NewAggregate().Aggregate, + []byte("template"), + ), + ), + ), + expectPush( + []*repository.Event{ + eventFromEventPusher( + newDefaultMailTemplatePolicyChangedEvent(context.Background(), []byte("template-change")), + ), + }, + ), + ), + }, + args: args{ + ctx: context.Background(), + policy: &domain.MailTemplate{ + Template: []byte("template-change"), + }, + }, + res: res{ + want: &domain.MailTemplate{ + ObjectRoot: models.ObjectRoot{ + AggregateID: "IAM", + ResourceOwner: "IAM", + }, + Template: []byte("template-change"), + }, + }, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + r := &Commands{ + eventstore: tt.fields.eventstore, + } + got, err := r.ChangeDefaultMailTemplate(tt.args.ctx, tt.args.policy) + 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 newDefaultMailTemplatePolicyChangedEvent(ctx context.Context, template []byte) *iam.MailTemplateChangedEvent { + event, _ := iam.NewMailTemplateChangedEvent(ctx, + &iam.NewAggregate().Aggregate, + []policy.MailTemplateChanges{ + policy.ChangeTemplate(template), + }, + ) + return event +} diff --git a/internal/command/iam_policy_mail_text.go b/internal/command/iam_policy_mail_text.go index 39064c56e..c65f7f21a 100644 --- a/internal/command/iam_policy_mail_text.go +++ b/internal/command/iam_policy_mail_text.go @@ -30,7 +30,7 @@ func (c *Commands) AddDefaultMailText(ctx context.Context, policy *domain.MailTe func (c *Commands) addDefaultMailText(ctx context.Context, iamAgg *eventstore.Aggregate, addedPolicy *IAMMailTextWriteModel, mailText *domain.MailText) (eventstore.EventPusher, error) { if !mailText.IsValid() { - return nil, caos_errs.ThrowPreconditionFailed(nil, "IAM-3n8fs", "Errors.IAM.MailText.Invalid") + return nil, caos_errs.ThrowInvalidArgument(nil, "IAM-3n8fs", "Errors.IAM.MailText.Invalid") } err := c.eventstore.FilterToQueryReducer(ctx, addedPolicy) if err != nil { @@ -55,7 +55,7 @@ func (c *Commands) addDefaultMailText(ctx context.Context, iamAgg *eventstore.Ag func (c *Commands) ChangeDefaultMailText(ctx context.Context, mailText *domain.MailText) (*domain.MailText, error) { if !mailText.IsValid() { - return nil, caos_errs.ThrowPreconditionFailed(nil, "IAM-kd9fs", "Errors.IAM.MailText.Invalid") + return nil, caos_errs.ThrowInvalidArgument(nil, "IAM-kd9fs", "Errors.IAM.MailText.Invalid") } existingPolicy, err := c.defaultMailTextWriteModelByID(ctx, mailText.MailTextType, mailText.Language) if err != nil { diff --git a/internal/command/iam_policy_mail_text_test.go b/internal/command/iam_policy_mail_text_test.go new file mode 100644 index 000000000..d63a1057f --- /dev/null +++ b/internal/command/iam_policy_mail_text_test.go @@ -0,0 +1,366 @@ +package command + +import ( + "context" + "github.com/caos/zitadel/internal/domain" + caos_errs "github.com/caos/zitadel/internal/errors" + "github.com/caos/zitadel/internal/eventstore" + "github.com/caos/zitadel/internal/eventstore/repository" + "github.com/caos/zitadel/internal/eventstore/v1/models" + "github.com/caos/zitadel/internal/repository/iam" + "github.com/caos/zitadel/internal/repository/policy" + "github.com/stretchr/testify/assert" + "testing" +) + +func TestCommandSide_AddDefaultMailTextPolicy(t *testing.T) { + type fields struct { + eventstore *eventstore.Eventstore + } + type args struct { + ctx context.Context + policy *domain.MailText + } + type res struct { + want *domain.MailText + err func(error) bool + } + tests := []struct { + name string + fields fields + args args + res res + }{ + { + name: "mail text invalid, invalid argument error", + fields: fields{ + eventstore: eventstoreExpect( + t, + ), + }, + args: args{ + ctx: context.Background(), + policy: &domain.MailText{}, + }, + res: res{ + err: caos_errs.IsErrorInvalidArgument, + }, + }, + { + name: "mail text already existing, already exists error", + fields: fields{ + eventstore: eventstoreExpect( + t, + expectFilter( + eventFromEventPusher( + iam.NewMailTextAddedEvent(context.Background(), + &iam.NewAggregate().Aggregate, + "mail-text-type", + "de", + "title", + "pre-header", + "subject", + "greeting", + "text", + "button-text", + ), + ), + ), + ), + }, + args: args{ + ctx: context.Background(), + policy: &domain.MailText{ + MailTextType: "mail-text-type", + Language: "de", + Title: "title", + PreHeader: "pre-header", + Subject: "subject", + Greeting: "greeting", + Text: "text", + ButtonText: "button-text", + }, + }, + res: res{ + err: caos_errs.IsErrorAlreadyExists, + }, + }, + { + name: "add mail template,ok", + fields: fields{ + eventstore: eventstoreExpect( + t, + expectFilter(), + expectPush( + []*repository.Event{ + eventFromEventPusher( + iam.NewMailTextAddedEvent(context.Background(), + &iam.NewAggregate().Aggregate, + "mail-text-type", + "de", + "title", + "pre-header", + "subject", + "greeting", + "text", + "button-text", + ), + ), + }, + uniqueConstraintsFromEventConstraint(policy.NewAddMailTextUniqueConstraint("IAM", "mail-text-type", "de")), + ), + ), + }, + args: args{ + ctx: context.Background(), + policy: &domain.MailText{ + MailTextType: "mail-text-type", + Language: "de", + Title: "title", + PreHeader: "pre-header", + Subject: "subject", + Greeting: "greeting", + Text: "text", + ButtonText: "button-text", + }, + }, + res: res{ + want: &domain.MailText{ + ObjectRoot: models.ObjectRoot{ + AggregateID: "IAM", + ResourceOwner: "IAM", + }, + MailTextType: "mail-text-type", + Language: "de", + Title: "title", + PreHeader: "pre-header", + Subject: "subject", + Greeting: "greeting", + Text: "text", + ButtonText: "button-text", + State: domain.PolicyStateActive, + }, + }, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + r := &Commands{ + eventstore: tt.fields.eventstore, + } + got, err := r.AddDefaultMailText(tt.args.ctx, tt.args.policy) + 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_ChangeDefaultMailTextPolicy(t *testing.T) { + type fields struct { + eventstore *eventstore.Eventstore + } + type args struct { + ctx context.Context + policy *domain.MailText + } + type res struct { + want *domain.MailText + err func(error) bool + } + tests := []struct { + name string + fields fields + args args + res res + }{ + { + name: "mailtext invalid, invalid argument error", + fields: fields{ + eventstore: eventstoreExpect( + t, + ), + }, + args: args{ + ctx: context.Background(), + policy: &domain.MailText{}, + }, + res: res{ + err: caos_errs.IsErrorInvalidArgument, + }, + }, + { + name: "mail text not existing, not found error", + fields: fields{ + eventstore: eventstoreExpect( + t, + expectFilter(), + ), + }, + args: args{ + ctx: context.Background(), + policy: &domain.MailText{ + MailTextType: "mail-text-type", + Language: "de", + Title: "title", + PreHeader: "pre-header", + Subject: "subject", + Greeting: "greeting", + Text: "text", + ButtonText: "button-text", + }, + }, + res: res{ + err: caos_errs.IsNotFound, + }, + }, + { + name: "no changes, precondition error", + fields: fields{ + eventstore: eventstoreExpect( + t, + expectFilter( + eventFromEventPusher( + iam.NewMailTextAddedEvent(context.Background(), + &iam.NewAggregate().Aggregate, + "mail-text-type", + "de", + "title", + "pre-header", + "subject", + "greeting", + "text", + "button-text", + ), + ), + ), + ), + }, + args: args{ + ctx: context.Background(), + policy: &domain.MailText{ + MailTextType: "mail-text-type", + Language: "de", + Title: "title", + PreHeader: "pre-header", + Subject: "subject", + Greeting: "greeting", + Text: "text", + ButtonText: "button-text", + }, + }, + res: res{ + err: caos_errs.IsPreconditionFailed, + }, + }, + { + name: "change, ok", + fields: fields{ + eventstore: eventstoreExpect( + t, + expectFilter( + eventFromEventPusher( + iam.NewMailTextAddedEvent(context.Background(), + &iam.NewAggregate().Aggregate, + "mail-text-type", + "de", + "title", + "pre-header", + "subject", + "greeting", + "text", + "button-text", + ), + ), + ), + expectPush( + []*repository.Event{ + eventFromEventPusher( + newDefaultMailTextPolicyChangedEvent( + context.Background(), + "mail-text-type", + "de", + "title-change", + "pre-header-change", + "subject-change", + "greeting-change", + "text-change", + "button-text-change"), + ), + }, + ), + ), + }, + args: args{ + ctx: context.Background(), + policy: &domain.MailText{ + MailTextType: "mail-text-type", + Language: "de", + Title: "title-change", + PreHeader: "pre-header-change", + Subject: "subject-change", + Greeting: "greeting-change", + Text: "text-change", + ButtonText: "button-text-change", + }, + }, + res: res{ + want: &domain.MailText{ + ObjectRoot: models.ObjectRoot{ + AggregateID: "IAM", + ResourceOwner: "IAM", + }, + MailTextType: "mail-text-type", + Language: "de", + Title: "title-change", + PreHeader: "pre-header-change", + Subject: "subject-change", + Greeting: "greeting-change", + Text: "text-change", + ButtonText: "button-text-change", + State: domain.PolicyStateActive, + }, + }, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + r := &Commands{ + eventstore: tt.fields.eventstore, + } + got, err := r.ChangeDefaultMailText(tt.args.ctx, tt.args.policy) + 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 newDefaultMailTextPolicyChangedEvent(ctx context.Context, mailTextType, language, title, preHeader, subject, greeting, text, buttonText string) *iam.MailTextChangedEvent { + event, _ := iam.NewMailTextChangedEvent(ctx, + &iam.NewAggregate().Aggregate, + mailTextType, + language, + []policy.MailTextChanges{ + policy.ChangeTitle(title), + policy.ChangePreHeader(preHeader), + policy.ChangeSubject(subject), + policy.ChangeGreeting(greeting), + policy.ChangeText(text), + policy.ChangeButtonText(buttonText), + }, + ) + return event +} diff --git a/internal/command/iam_policy_org_iam_test.go b/internal/command/iam_policy_org_iam_test.go new file mode 100644 index 000000000..358a159b7 --- /dev/null +++ b/internal/command/iam_policy_org_iam_test.go @@ -0,0 +1,240 @@ +package command + +import ( + "context" + "github.com/caos/zitadel/internal/domain" + caos_errs "github.com/caos/zitadel/internal/errors" + "github.com/caos/zitadel/internal/eventstore" + "github.com/caos/zitadel/internal/eventstore/repository" + "github.com/caos/zitadel/internal/eventstore/v1/models" + "github.com/caos/zitadel/internal/repository/iam" + "github.com/caos/zitadel/internal/repository/policy" + "github.com/stretchr/testify/assert" + "testing" +) + +func TestCommandSide_AddDefaultOrgIAMPolicy(t *testing.T) { + type fields struct { + eventstore *eventstore.Eventstore + } + type args struct { + ctx context.Context + policy *domain.OrgIAMPolicy + } + type res struct { + want *domain.OrgIAMPolicy + err func(error) bool + } + tests := []struct { + name string + fields fields + args args + res res + }{ + { + name: "orgiam policy already existing, already exists error", + fields: fields{ + eventstore: eventstoreExpect( + t, + expectFilter( + eventFromEventPusher( + iam.NewOrgIAMPolicyAddedEvent(context.Background(), + &iam.NewAggregate().Aggregate, + true, + ), + ), + ), + ), + }, + args: args{ + ctx: context.Background(), + policy: &domain.OrgIAMPolicy{ + UserLoginMustBeDomain: true, + }, + }, + res: res{ + err: caos_errs.IsErrorAlreadyExists, + }, + }, + { + name: "add policy,ok", + fields: fields{ + eventstore: eventstoreExpect( + t, + expectFilter(), + expectPush( + []*repository.Event{ + eventFromEventPusher( + iam.NewOrgIAMPolicyAddedEvent(context.Background(), + &iam.NewAggregate().Aggregate, + true, + ), + ), + }, + ), + ), + }, + args: args{ + ctx: context.Background(), + policy: &domain.OrgIAMPolicy{ + UserLoginMustBeDomain: true, + }, + }, + res: res{ + want: &domain.OrgIAMPolicy{ + ObjectRoot: models.ObjectRoot{ + AggregateID: "IAM", + ResourceOwner: "IAM", + }, + UserLoginMustBeDomain: true, + }, + }, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + r := &Commands{ + eventstore: tt.fields.eventstore, + } + got, err := r.AddDefaultOrgIAMPolicy(tt.args.ctx, tt.args.policy) + 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_ChangeDefaultOrgIAMPolicy(t *testing.T) { + type fields struct { + eventstore *eventstore.Eventstore + } + type args struct { + ctx context.Context + policy *domain.OrgIAMPolicy + } + type res struct { + want *domain.OrgIAMPolicy + err func(error) bool + } + tests := []struct { + name string + fields fields + args args + res res + }{ + { + name: "orgiampolicy not existing, not found error", + fields: fields{ + eventstore: eventstoreExpect( + t, + expectFilter(), + ), + }, + args: args{ + ctx: context.Background(), + policy: &domain.OrgIAMPolicy{ + UserLoginMustBeDomain: true, + }, + }, + res: res{ + err: caos_errs.IsNotFound, + }, + }, + { + name: "no changes, precondition error", + fields: fields{ + eventstore: eventstoreExpect( + t, + expectFilter( + eventFromEventPusher( + iam.NewOrgIAMPolicyAddedEvent(context.Background(), + &iam.NewAggregate().Aggregate, + true, + ), + ), + ), + ), + }, + args: args{ + ctx: context.Background(), + policy: &domain.OrgIAMPolicy{ + UserLoginMustBeDomain: true, + }, + }, + res: res{ + err: caos_errs.IsPreconditionFailed, + }, + }, + { + name: "change, ok", + fields: fields{ + eventstore: eventstoreExpect( + t, + expectFilter( + eventFromEventPusher( + iam.NewOrgIAMPolicyAddedEvent(context.Background(), + &iam.NewAggregate().Aggregate, + true, + ), + ), + ), + expectPush( + []*repository.Event{ + eventFromEventPusher( + newDefaultOrgIAMPolicyChangedEvent(context.Background(), false), + ), + }, + ), + ), + }, + args: args{ + ctx: context.Background(), + policy: &domain.OrgIAMPolicy{ + UserLoginMustBeDomain: false, + }, + }, + res: res{ + want: &domain.OrgIAMPolicy{ + ObjectRoot: models.ObjectRoot{ + AggregateID: "IAM", + ResourceOwner: "IAM", + }, + UserLoginMustBeDomain: false, + }, + }, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + r := &Commands{ + eventstore: tt.fields.eventstore, + } + got, err := r.ChangeDefaultOrgIAMPolicy(tt.args.ctx, tt.args.policy) + 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 newDefaultOrgIAMPolicyChangedEvent(ctx context.Context, userLoginMustBeDomain bool) *iam.OrgIAMPolicyChangedEvent { + event, _ := iam.NewOrgIAMPolicyChangedEvent(ctx, + &iam.NewAggregate().Aggregate, + []policy.OrgIAMPolicyChanges{ + policy.ChangeUserLoginMustBeDomain(userLoginMustBeDomain), + }, + ) + return event +} diff --git a/internal/command/iam_policy_password_age_test.go b/internal/command/iam_policy_password_age_test.go new file mode 100644 index 000000000..672489afe --- /dev/null +++ b/internal/command/iam_policy_password_age_test.go @@ -0,0 +1,252 @@ +package command + +import ( + "context" + "github.com/caos/zitadel/internal/domain" + caos_errs "github.com/caos/zitadel/internal/errors" + "github.com/caos/zitadel/internal/eventstore" + "github.com/caos/zitadel/internal/eventstore/repository" + "github.com/caos/zitadel/internal/eventstore/v1/models" + "github.com/caos/zitadel/internal/repository/iam" + "github.com/caos/zitadel/internal/repository/policy" + "github.com/stretchr/testify/assert" + "testing" +) + +func TestCommandSide_AddDefaultPasswordAgePolicy(t *testing.T) { + type fields struct { + eventstore *eventstore.Eventstore + } + type args struct { + ctx context.Context + policy *domain.PasswordAgePolicy + } + type res struct { + want *domain.PasswordAgePolicy + err func(error) bool + } + tests := []struct { + name string + fields fields + args args + res res + }{ + { + name: "password age policy already existing, already exists error", + fields: fields{ + eventstore: eventstoreExpect( + t, + expectFilter( + eventFromEventPusher( + iam.NewPasswordAgePolicyAddedEvent(context.Background(), + &iam.NewAggregate().Aggregate, + 365, + 10, + ), + ), + ), + ), + }, + args: args{ + ctx: context.Background(), + policy: &domain.PasswordAgePolicy{ + MaxAgeDays: 365, + ExpireWarnDays: 10, + }, + }, + res: res{ + err: caos_errs.IsErrorAlreadyExists, + }, + }, + { + name: "add policy,ok", + fields: fields{ + eventstore: eventstoreExpect( + t, + expectFilter(), + expectPush( + []*repository.Event{ + eventFromEventPusher( + iam.NewPasswordAgePolicyAddedEvent(context.Background(), + &iam.NewAggregate().Aggregate, + 365, + 10, + ), + ), + }, + ), + ), + }, + args: args{ + ctx: context.Background(), + policy: &domain.PasswordAgePolicy{ + ExpireWarnDays: 365, + MaxAgeDays: 10, + }, + }, + res: res{ + want: &domain.PasswordAgePolicy{ + ObjectRoot: models.ObjectRoot{ + AggregateID: "IAM", + ResourceOwner: "IAM", + }, + ExpireWarnDays: 365, + MaxAgeDays: 10, + }, + }, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + r := &Commands{ + eventstore: tt.fields.eventstore, + } + got, err := r.AddDefaultPasswordAgePolicy(tt.args.ctx, tt.args.policy) + 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_ChangeDefaultPasswordAgePolicy(t *testing.T) { + type fields struct { + eventstore *eventstore.Eventstore + } + type args struct { + ctx context.Context + policy *domain.PasswordAgePolicy + } + type res struct { + want *domain.PasswordAgePolicy + err func(error) bool + } + tests := []struct { + name string + fields fields + args args + res res + }{ + { + name: "password age policy not existing, not found error", + fields: fields{ + eventstore: eventstoreExpect( + t, + expectFilter(), + ), + }, + args: args{ + ctx: context.Background(), + policy: &domain.PasswordAgePolicy{ + MaxAgeDays: 365, + ExpireWarnDays: 10, + }, + }, + res: res{ + err: caos_errs.IsNotFound, + }, + }, + { + name: "no changes, precondition error", + fields: fields{ + eventstore: eventstoreExpect( + t, + expectFilter( + eventFromEventPusher( + iam.NewPasswordAgePolicyAddedEvent(context.Background(), + &iam.NewAggregate().Aggregate, + 365, + 10, + ), + ), + ), + ), + }, + args: args{ + ctx: context.Background(), + policy: &domain.PasswordAgePolicy{ + ExpireWarnDays: 365, + MaxAgeDays: 10, + }, + }, + res: res{ + err: caos_errs.IsPreconditionFailed, + }, + }, + { + name: "change, ok", + fields: fields{ + eventstore: eventstoreExpect( + t, + expectFilter( + eventFromEventPusher( + iam.NewPasswordAgePolicyAddedEvent(context.Background(), + &iam.NewAggregate().Aggregate, + 365, + 10, + ), + ), + ), + expectPush( + []*repository.Event{ + eventFromEventPusher( + newDefaultPasswordAgePolicyChangedEvent(context.Background(), 125, 5), + ), + }, + ), + ), + }, + args: args{ + ctx: context.Background(), + policy: &domain.PasswordAgePolicy{ + MaxAgeDays: 125, + ExpireWarnDays: 5, + }, + }, + res: res{ + want: &domain.PasswordAgePolicy{ + ObjectRoot: models.ObjectRoot{ + AggregateID: "IAM", + ResourceOwner: "IAM", + }, + MaxAgeDays: 125, + ExpireWarnDays: 5, + }, + }, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + r := &Commands{ + eventstore: tt.fields.eventstore, + } + got, err := r.ChangeDefaultPasswordAgePolicy(tt.args.ctx, tt.args.policy) + 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 newDefaultPasswordAgePolicyChangedEvent(ctx context.Context, maxAgeDays, expiryWarnDays uint64) *iam.PasswordAgePolicyChangedEvent { + event, _ := iam.NewPasswordAgePolicyChangedEvent(ctx, + &iam.NewAggregate().Aggregate, + []policy.PasswordAgePolicyChanges{ + policy.ChangeExpireWarnDays(expiryWarnDays), + policy.ChangeMaxAgeDays(maxAgeDays), + }, + ) + return event +} diff --git a/internal/command/iam_policy_password_complexity_test.go b/internal/command/iam_policy_password_complexity_test.go new file mode 100644 index 000000000..627e90e14 --- /dev/null +++ b/internal/command/iam_policy_password_complexity_test.go @@ -0,0 +1,318 @@ +package command + +import ( + "context" + "github.com/caos/zitadel/internal/domain" + caos_errs "github.com/caos/zitadel/internal/errors" + "github.com/caos/zitadel/internal/eventstore" + "github.com/caos/zitadel/internal/eventstore/repository" + "github.com/caos/zitadel/internal/eventstore/v1/models" + "github.com/caos/zitadel/internal/repository/iam" + "github.com/caos/zitadel/internal/repository/policy" + "github.com/stretchr/testify/assert" + "testing" +) + +func TestCommandSide_AddDefaultPasswordComplexityPolicy(t *testing.T) { + type fields struct { + eventstore *eventstore.Eventstore + } + type args struct { + ctx context.Context + policy *domain.PasswordComplexityPolicy + } + type res struct { + want *domain.PasswordComplexityPolicy + err func(error) bool + } + tests := []struct { + name string + fields fields + args args + res res + }{ + { + name: "invalid policy, invalid argument error", + fields: fields{ + eventstore: eventstoreExpect( + t, + ), + }, + args: args{ + ctx: context.Background(), + policy: &domain.PasswordComplexityPolicy{ + MinLength: 0, + HasUppercase: true, + HasLowercase: true, + HasNumber: true, + HasSymbol: true, + }, + }, + res: res{ + err: caos_errs.IsErrorInvalidArgument, + }, + }, + { + name: "password complexity policy already existing, already exists error", + fields: fields{ + eventstore: eventstoreExpect( + t, + expectFilter( + eventFromEventPusher( + iam.NewPasswordComplexityPolicyAddedEvent(context.Background(), + &iam.NewAggregate().Aggregate, + 8, + true, true, true, true, + ), + ), + ), + ), + }, + args: args{ + ctx: context.Background(), + policy: &domain.PasswordComplexityPolicy{ + MinLength: 8, + HasUppercase: true, + HasLowercase: true, + HasNumber: true, + HasSymbol: true, + }, + }, + res: res{ + err: caos_errs.IsErrorAlreadyExists, + }, + }, + { + name: "add policy,ok", + fields: fields{ + eventstore: eventstoreExpect( + t, + expectFilter(), + expectPush( + []*repository.Event{ + eventFromEventPusher( + iam.NewPasswordComplexityPolicyAddedEvent(context.Background(), + &iam.NewAggregate().Aggregate, + 8, + true, true, true, true, + ), + ), + }, + ), + ), + }, + args: args{ + ctx: context.Background(), + policy: &domain.PasswordComplexityPolicy{ + MinLength: 8, + HasUppercase: true, + HasLowercase: true, + HasNumber: true, + HasSymbol: true, + }, + }, + res: res{ + want: &domain.PasswordComplexityPolicy{ + ObjectRoot: models.ObjectRoot{ + AggregateID: "IAM", + ResourceOwner: "IAM", + }, + MinLength: 8, + HasUppercase: true, + HasLowercase: true, + HasNumber: true, + HasSymbol: true, + }, + }, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + r := &Commands{ + eventstore: tt.fields.eventstore, + } + got, err := r.AddDefaultPasswordComplexityPolicy(tt.args.ctx, tt.args.policy) + 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_ChangeDefaultPasswordComplexityPolicy(t *testing.T) { + type fields struct { + eventstore *eventstore.Eventstore + } + type args struct { + ctx context.Context + policy *domain.PasswordComplexityPolicy + } + type res struct { + want *domain.PasswordComplexityPolicy + err func(error) bool + } + tests := []struct { + name string + fields fields + args args + res res + }{ + { + name: "invalid policy, invalid argument error", + fields: fields{ + eventstore: eventstoreExpect( + t, + ), + }, + args: args{ + ctx: context.Background(), + policy: &domain.PasswordComplexityPolicy{ + MinLength: 0, + HasUppercase: true, + HasLowercase: true, + HasNumber: true, + HasSymbol: true, + }, + }, + res: res{ + err: caos_errs.IsErrorInvalidArgument, + }, + }, + { + name: "password complexity policy not existing, not found error", + fields: fields{ + eventstore: eventstoreExpect( + t, + expectFilter(), + ), + }, + args: args{ + ctx: context.Background(), + policy: &domain.PasswordComplexityPolicy{ + MinLength: 8, + HasUppercase: true, + HasLowercase: true, + HasNumber: true, + HasSymbol: true, + }, + }, + res: res{ + err: caos_errs.IsNotFound, + }, + }, + { + name: "no changes, precondition error", + fields: fields{ + eventstore: eventstoreExpect( + t, + expectFilter( + eventFromEventPusher( + iam.NewPasswordComplexityPolicyAddedEvent(context.Background(), + &iam.NewAggregate().Aggregate, + 8, + true, true, true, true, + ), + ), + ), + ), + }, + args: args{ + ctx: context.Background(), + policy: &domain.PasswordComplexityPolicy{ + MinLength: 8, + HasUppercase: true, + HasLowercase: true, + HasNumber: true, + HasSymbol: true, + }, + }, + res: res{ + err: caos_errs.IsPreconditionFailed, + }, + }, + { + name: "change, ok", + fields: fields{ + eventstore: eventstoreExpect( + t, + expectFilter( + eventFromEventPusher( + iam.NewPasswordComplexityPolicyAddedEvent(context.Background(), + &iam.NewAggregate().Aggregate, + 8, + true, true, true, true, + ), + ), + ), + expectPush( + []*repository.Event{ + eventFromEventPusher( + newDefaultPasswordComplexityPolicyChangedEvent(context.Background(), 10, false, false, false, false), + ), + }, + ), + ), + }, + args: args{ + ctx: context.Background(), + policy: &domain.PasswordComplexityPolicy{ + MinLength: 10, + HasUppercase: false, + HasLowercase: false, + HasNumber: false, + HasSymbol: false, + }, + }, + res: res{ + want: &domain.PasswordComplexityPolicy{ + ObjectRoot: models.ObjectRoot{ + AggregateID: "IAM", + ResourceOwner: "IAM", + }, + MinLength: 10, + HasUppercase: false, + HasLowercase: false, + HasNumber: false, + HasSymbol: false, + }, + }, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + r := &Commands{ + eventstore: tt.fields.eventstore, + } + got, err := r.ChangeDefaultPasswordComplexityPolicy(tt.args.ctx, tt.args.policy) + 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 newDefaultPasswordComplexityPolicyChangedEvent(ctx context.Context, minLength uint64, hasUpper, hasLower, hasNumber, hasSymbol bool) *iam.PasswordComplexityPolicyChangedEvent { + event, _ := iam.NewPasswordComplexityPolicyChangedEvent(ctx, + &iam.NewAggregate().Aggregate, + []policy.PasswordComplexityPolicyChanges{ + policy.ChangeMinLength(minLength), + policy.ChangeHasUppercase(hasUpper), + policy.ChangeHasLowercase(hasLower), + policy.ChangeHasSymbol(hasNumber), + policy.ChangeHasNumber(hasSymbol), + }, + ) + return event +} diff --git a/internal/command/iam_policy_password_lockout_test.go b/internal/command/iam_policy_password_lockout_test.go new file mode 100644 index 000000000..97fe84e3e --- /dev/null +++ b/internal/command/iam_policy_password_lockout_test.go @@ -0,0 +1,252 @@ +package command + +import ( + "context" + "github.com/caos/zitadel/internal/domain" + caos_errs "github.com/caos/zitadel/internal/errors" + "github.com/caos/zitadel/internal/eventstore" + "github.com/caos/zitadel/internal/eventstore/repository" + "github.com/caos/zitadel/internal/eventstore/v1/models" + "github.com/caos/zitadel/internal/repository/iam" + "github.com/caos/zitadel/internal/repository/policy" + "github.com/stretchr/testify/assert" + "testing" +) + +func TestCommandSide_AddDefaultPasswordLockoutPolicy(t *testing.T) { + type fields struct { + eventstore *eventstore.Eventstore + } + type args struct { + ctx context.Context + policy *domain.PasswordLockoutPolicy + } + type res struct { + want *domain.PasswordLockoutPolicy + err func(error) bool + } + tests := []struct { + name string + fields fields + args args + res res + }{ + { + name: "password lockout policy already existing, already exists error", + fields: fields{ + eventstore: eventstoreExpect( + t, + expectFilter( + eventFromEventPusher( + iam.NewPasswordLockoutPolicyAddedEvent(context.Background(), + &iam.NewAggregate().Aggregate, + 10, + true, + ), + ), + ), + ), + }, + args: args{ + ctx: context.Background(), + policy: &domain.PasswordLockoutPolicy{ + MaxAttempts: 10, + ShowLockOutFailures: true, + }, + }, + res: res{ + err: caos_errs.IsErrorAlreadyExists, + }, + }, + { + name: "add policy,ok", + fields: fields{ + eventstore: eventstoreExpect( + t, + expectFilter(), + expectPush( + []*repository.Event{ + eventFromEventPusher( + iam.NewPasswordLockoutPolicyAddedEvent(context.Background(), + &iam.NewAggregate().Aggregate, + 10, + true, + ), + ), + }, + ), + ), + }, + args: args{ + ctx: context.Background(), + policy: &domain.PasswordLockoutPolicy{ + MaxAttempts: 10, + ShowLockOutFailures: true, + }, + }, + res: res{ + want: &domain.PasswordLockoutPolicy{ + ObjectRoot: models.ObjectRoot{ + AggregateID: "IAM", + ResourceOwner: "IAM", + }, + MaxAttempts: 10, + ShowLockOutFailures: true, + }, + }, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + r := &Commands{ + eventstore: tt.fields.eventstore, + } + got, err := r.AddDefaultPasswordLockoutPolicy(tt.args.ctx, tt.args.policy) + 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_ChangeDefaultPasswordLockoutPolicy(t *testing.T) { + type fields struct { + eventstore *eventstore.Eventstore + } + type args struct { + ctx context.Context + policy *domain.PasswordLockoutPolicy + } + type res struct { + want *domain.PasswordLockoutPolicy + err func(error) bool + } + tests := []struct { + name string + fields fields + args args + res res + }{ + { + name: "password lockout policy not existing, not found error", + fields: fields{ + eventstore: eventstoreExpect( + t, + expectFilter(), + ), + }, + args: args{ + ctx: context.Background(), + policy: &domain.PasswordLockoutPolicy{ + MaxAttempts: 10, + ShowLockOutFailures: true, + }, + }, + res: res{ + err: caos_errs.IsNotFound, + }, + }, + { + name: "no changes, precondition error", + fields: fields{ + eventstore: eventstoreExpect( + t, + expectFilter( + eventFromEventPusher( + iam.NewPasswordLockoutPolicyAddedEvent(context.Background(), + &iam.NewAggregate().Aggregate, + 10, + true, + ), + ), + ), + ), + }, + args: args{ + ctx: context.Background(), + policy: &domain.PasswordLockoutPolicy{ + MaxAttempts: 10, + ShowLockOutFailures: true, + }, + }, + res: res{ + err: caos_errs.IsPreconditionFailed, + }, + }, + { + name: "change, ok", + fields: fields{ + eventstore: eventstoreExpect( + t, + expectFilter( + eventFromEventPusher( + iam.NewPasswordLockoutPolicyAddedEvent(context.Background(), + &iam.NewAggregate().Aggregate, + 10, + true, + ), + ), + ), + expectPush( + []*repository.Event{ + eventFromEventPusher( + newDefaultPasswordLockoutPolicyChangedEvent(context.Background(), 20, false), + ), + }, + ), + ), + }, + args: args{ + ctx: context.Background(), + policy: &domain.PasswordLockoutPolicy{ + MaxAttempts: 20, + ShowLockOutFailures: false, + }, + }, + res: res{ + want: &domain.PasswordLockoutPolicy{ + ObjectRoot: models.ObjectRoot{ + AggregateID: "IAM", + ResourceOwner: "IAM", + }, + MaxAttempts: 20, + ShowLockOutFailures: false, + }, + }, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + r := &Commands{ + eventstore: tt.fields.eventstore, + } + got, err := r.ChangeDefaultPasswordLockoutPolicy(tt.args.ctx, tt.args.policy) + 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 newDefaultPasswordLockoutPolicyChangedEvent(ctx context.Context, maxAttempts uint64, showLockoutFailure bool) *iam.PasswordLockoutPolicyChangedEvent { + event, _ := iam.NewPasswordLockoutPolicyChangedEvent(ctx, + &iam.NewAggregate().Aggregate, + []policy.PasswordLockoutPolicyChanges{ + policy.ChangeMaxAttempts(maxAttempts), + policy.ChangeShowLockOutFailures(showLockoutFailure), + }, + ) + return event +} diff --git a/internal/command/main_test.go b/internal/command/main_test.go new file mode 100644 index 000000000..a9b79e1cf --- /dev/null +++ b/internal/command/main_test.go @@ -0,0 +1,168 @@ +package command + +import ( + "context" + "github.com/caos/zitadel/internal/crypto" + "github.com/caos/zitadel/internal/eventstore" + "github.com/caos/zitadel/internal/eventstore/repository" + "github.com/caos/zitadel/internal/eventstore/repository/mock" + iam_repo "github.com/caos/zitadel/internal/repository/iam" + key_repo "github.com/caos/zitadel/internal/repository/keypair" + "github.com/caos/zitadel/internal/repository/org" + proj_repo "github.com/caos/zitadel/internal/repository/project" + usr_repo "github.com/caos/zitadel/internal/repository/user" + "github.com/caos/zitadel/internal/repository/usergrant" + "github.com/golang/mock/gomock" + "testing" + "time" +) + +type expect func(mockRepository *mock.MockRepository) + +func eventstoreExpect(t *testing.T, expects ...expect) *eventstore.Eventstore { + m := mock.NewRepo(t) + for _, e := range expects { + e(m) + } + es := eventstore.NewEventstore(m) + iam_repo.RegisterEventMappers(es) + org.RegisterEventMappers(es) + usr_repo.RegisterEventMappers(es) + proj_repo.RegisterEventMappers(es) + usergrant.RegisterEventMappers(es) + key_repo.RegisterEventMappers(es) + return es +} + +func eventPusherToEvents(eventsPushes ...eventstore.EventPusher) []*repository.Event { + events := make([]*repository.Event, len(eventsPushes)) + for i, event := range eventsPushes { + data, err := eventstore.EventData(event) + if err != nil { + return nil + } + events[i] = &repository.Event{ + AggregateID: event.Aggregate().ID, + AggregateType: repository.AggregateType(event.Aggregate().Typ), + ResourceOwner: event.Aggregate().ResourceOwner, + EditorService: event.EditorService(), + EditorUser: event.EditorUser(), + Type: repository.EventType(event.Type()), + Version: repository.Version(event.Aggregate().Version), + Data: data, + } + } + return events +} + +type testRepo struct { + events []*repository.Event + uniqueConstraints []*repository.UniqueConstraint + sequence uint64 + err error + t *testing.T +} + +func (repo *testRepo) Health(ctx context.Context) error { + return nil +} + +func (repo *testRepo) Push(ctx context.Context, events []*repository.Event, uniqueConstraints ...*repository.UniqueConstraint) error { + repo.events = append(repo.events, events...) + repo.uniqueConstraints = append(repo.uniqueConstraints, uniqueConstraints...) + return nil +} + +func (repo *testRepo) Filter(ctx context.Context, searchQuery *repository.SearchQuery) ([]*repository.Event, error) { + events := make([]*repository.Event, 0, len(repo.events)) + for _, event := range repo.events { + for _, filter := range searchQuery.Filters { + if filter.Field == repository.FieldAggregateType { + if event.AggregateType != filter.Value { + continue + } + } + } + events = append(events, event) + } + return repo.events, nil +} + +func filterAggregateType(aggregateType string) { + +} + +func (repo *testRepo) LatestSequence(ctx context.Context, queryFactory *repository.SearchQuery) (uint64, error) { + if repo.err != nil { + return 0, repo.err + } + return repo.sequence, nil +} + +func expectPush(events []*repository.Event, uniqueConstraints ...*repository.UniqueConstraint) expect { + return func(m *mock.MockRepository) { + m.ExpectPush(events, uniqueConstraints...) + } +} + +func expectPushFailed(err error, events []*repository.Event, uniqueConstraints ...*repository.UniqueConstraint) expect { + return func(m *mock.MockRepository) { + m.ExpectPushFailed(err, events, uniqueConstraints...) + } +} + +func expectFilter(events ...*repository.Event) expect { + return func(m *mock.MockRepository) { + m.ExpectFilterEvents(events...) + } +} + +func expectFilterOrgDomainNotFound() expect { + return func(m *mock.MockRepository) { + m.ExpectFilterNoEventsNoError() + } +} + +func expectFilterOrgMemberNotFound() expect { + return func(m *mock.MockRepository) { + m.ExpectFilterNoEventsNoError() + } +} + +func eventFromEventPusher(event eventstore.EventPusher) *repository.Event { + data, _ := eventstore.EventData(event) + return &repository.Event{ + ID: "", + Sequence: 0, + PreviousSequence: 0, + CreationDate: time.Time{}, + Type: repository.EventType(event.Type()), + Data: data, + EditorService: event.EditorService(), + EditorUser: event.EditorUser(), + Version: repository.Version(event.Aggregate().Version), + AggregateID: event.Aggregate().ID, + AggregateType: repository.AggregateType(event.Aggregate().Typ), + ResourceOwner: event.Aggregate().ResourceOwner, + } +} + +func uniqueConstraintsFromEventConstraint(constraint *eventstore.EventUniqueConstraint) *repository.UniqueConstraint { + return &repository.UniqueConstraint{ + UniqueType: constraint.UniqueType, + UniqueField: constraint.UniqueField, + ErrorMessage: constraint.ErrorMessage, + Action: repository.UniqueConstraintAction(constraint.Action)} +} + +func GetMockSecretGenerator(t *testing.T) crypto.Generator { + ctrl := gomock.NewController(t) + alg := crypto.CreateMockEncryptionAlg(ctrl) + generator := crypto.NewMockGenerator(ctrl) + generator.EXPECT().Length().Return(uint(1)).AnyTimes() + generator.EXPECT().Runes().Return([]rune("aa")).AnyTimes() + generator.EXPECT().Alg().Return(alg).AnyTimes() + generator.EXPECT().Expiry().Return(time.Hour * 1).AnyTimes() + + return generator +} diff --git a/internal/command/org.go b/internal/command/org.go index 1cd4334b7..4c1e7d6d2 100644 --- a/internal/command/org.go +++ b/internal/command/org.go @@ -82,7 +82,7 @@ func (c *Commands) DeactivateOrg(ctx context.Context, orgID string) (*domain.Obj return nil, caos_errs.ThrowNotFound(nil, "ORG-oL9nT", "Errors.Org.NotFound") } if orgWriteModel.State == domain.OrgStateInactive { - return nil, caos_errs.ThrowInvalidArgument(nil, "EVENT-Dbs2g", "Errors.Org.AlreadyDeactivated") + return nil, caos_errs.ThrowPreconditionFailed(nil, "EVENT-Dbs2g", "Errors.Org.AlreadyDeactivated") } orgAgg := OrgAggregateFromWriteModel(&orgWriteModel.WriteModel) pushedEvents, err := c.eventstore.PushEvents(ctx, org.NewOrgDeactivatedEvent(ctx, orgAgg)) @@ -105,7 +105,7 @@ func (c *Commands) ReactivateOrg(ctx context.Context, orgID string) (*domain.Obj return nil, caos_errs.ThrowNotFound(nil, "ORG-Dgf3g", "Errors.Org.NotFound") } if orgWriteModel.State == domain.OrgStateActive { - return nil, caos_errs.ThrowInvalidArgument(nil, "EVENT-bfnrh", "Errors.Org.AlreadyActive") + return nil, caos_errs.ThrowPreconditionFailed(nil, "EVENT-bfnrh", "Errors.Org.AlreadyActive") } orgAgg := OrgAggregateFromWriteModel(&orgWriteModel.WriteModel) pushedEvents, err := c.eventstore.PushEvents(ctx, org.NewOrgReactivatedEvent(ctx, orgAgg)) diff --git a/internal/command/org_member.go b/internal/command/org_member.go index 62b4156c6..71f1c659a 100644 --- a/internal/command/org_member.go +++ b/internal/command/org_member.go @@ -19,7 +19,6 @@ func (c *Commands) AddOrgMember(ctx context.Context, member *domain.Member) (*do if err != nil { return nil, err } - pushedEvents, err := c.eventstore.PushEvents(ctx, event) if err != nil { return nil, err @@ -33,13 +32,16 @@ func (c *Commands) AddOrgMember(ctx context.Context, member *domain.Member) (*do func (c *Commands) addOrgMember(ctx context.Context, orgAgg *eventstore.Aggregate, addedMember *OrgMemberWriteModel, member *domain.Member) (eventstore.EventPusher, error) { if !member.IsValid() { - return nil, caos_errs.ThrowPreconditionFailed(nil, "Org-W8m4l", "Errors.Org.MemberInvalid") + return nil, caos_errs.ThrowInvalidArgument(nil, "Org-W8m4l", "Errors.Org.MemberInvalid") } if len(domain.CheckForInvalidRoles(member.Roles, domain.OrgRolePrefix, c.zitadelRoles)) > 0 { - return nil, caos_errs.ThrowPreconditionFailed(nil, "IAM-3m9fs", "Errors.Org.MemberInvalid") + return nil, caos_errs.ThrowInvalidArgument(nil, "IAM-3m9fs", "Errors.Org.MemberInvalid") } - - err := c.eventstore.FilterToQueryReducer(ctx, addedMember) + err := c.checkUserExists(ctx, addedMember.UserID, "") + if err != nil { + return nil, caos_errs.ThrowPreconditionFailed(err, "ORG-9cmsd", "Errors.User.NotFound") + } + err = c.eventstore.FilterToQueryReducer(ctx, addedMember) if err != nil { return nil, err } @@ -53,10 +55,10 @@ func (c *Commands) addOrgMember(ctx context.Context, orgAgg *eventstore.Aggregat //ChangeOrgMember updates an existing member func (c *Commands) ChangeOrgMember(ctx context.Context, member *domain.Member) (*domain.Member, error) { if !member.IsValid() { - return nil, caos_errs.ThrowPreconditionFailed(nil, "Org-LiaZi", "Errors.Org.MemberInvalid") + return nil, caos_errs.ThrowInvalidArgument(nil, "Org-LiaZi", "Errors.Org.MemberInvalid") } if len(domain.CheckForInvalidRoles(member.Roles, domain.OrgRolePrefix, c.zitadelRoles)) > 0 { - return nil, caos_errs.ThrowPreconditionFailed(nil, "IAM-m9fG8", "Errors.Org.MemberInvalid") + return nil, caos_errs.ThrowInvalidArgument(nil, "IAM-m9fG8", "Errors.Org.MemberInvalid") } existingMember, err := c.orgMemberWriteModelByID(ctx, member.AggregateID, member.UserID) diff --git a/internal/command/org_member_test.go b/internal/command/org_member_test.go new file mode 100644 index 000000000..2a00c1fb3 --- /dev/null +++ b/internal/command/org_member_test.go @@ -0,0 +1,615 @@ +package command + +import ( + "context" + "testing" + + "github.com/stretchr/testify/assert" + "golang.org/x/text/language" + + "github.com/caos/zitadel/internal/api/authz" + "github.com/caos/zitadel/internal/domain" + caos_errs "github.com/caos/zitadel/internal/errors" + "github.com/caos/zitadel/internal/eventstore" + "github.com/caos/zitadel/internal/eventstore/repository" + "github.com/caos/zitadel/internal/eventstore/v1/models" + "github.com/caos/zitadel/internal/repository/member" + "github.com/caos/zitadel/internal/repository/org" + "github.com/caos/zitadel/internal/repository/project" + "github.com/caos/zitadel/internal/repository/user" +) + +func TestCommandSide_AddOrgMember(t *testing.T) { + type fields struct { + eventstore *eventstore.Eventstore + zitadelRoles []authz.RoleMapping + } + type args struct { + ctx context.Context + member *domain.Member + } + type res struct { + want *domain.Member + err func(error) bool + } + tests := []struct { + name string + fields fields + args args + res res + }{ + { + name: "invalid member, error", + fields: fields{ + eventstore: eventstoreExpect( + t, + ), + }, + args: args{ + ctx: context.Background(), + member: &domain.Member{ + ObjectRoot: models.ObjectRoot{ + AggregateID: "org1", + }, + }, + }, + res: res{ + err: caos_errs.IsErrorInvalidArgument, + }, + }, + { + name: "invalid roles, error", + fields: fields{ + eventstore: eventstoreExpect( + t, + ), + }, + args: args{ + ctx: context.Background(), + member: &domain.Member{ + ObjectRoot: models.ObjectRoot{ + AggregateID: "org1", + }, + UserID: "user1", + Roles: []string{"ORG_OWNER"}, + }, + }, + res: res{ + err: caos_errs.IsErrorInvalidArgument, + }, + }, + { + name: "user not existing, precondition error", + fields: fields{ + eventstore: eventstoreExpect( + t, + expectFilter(), + ), + zitadelRoles: []authz.RoleMapping{ + { + Role: domain.RoleOrgOwner, + }, + }, + }, + args: args{ + ctx: context.Background(), + member: &domain.Member{ + ObjectRoot: models.ObjectRoot{ + AggregateID: "org1", + }, + UserID: "user1", + Roles: []string{domain.RoleOrgOwner}, + }, + }, + res: res{ + err: caos_errs.IsPreconditionFailed, + }, + }, + { + name: "member already exists, precondition error", + fields: fields{ + eventstore: eventstoreExpect( + t, + expectFilter( + eventFromEventPusher( + user.NewHumanAddedEvent(context.Background(), + &user.NewAggregate("user1", "org1").Aggregate, + "username1", + "firstname1", + "lastname1", + "nickname1", + "displayname1", + language.German, + domain.GenderMale, + "email1", + true, + ), + ), + ), + expectFilter( + eventFromEventPusher( + org.NewMemberAddedEvent(context.Background(), + &org.NewAggregate("org1", "org1").Aggregate, + "user1", + ), + ), + ), + ), + zitadelRoles: []authz.RoleMapping{ + { + Role: domain.RoleOrgOwner, + }, + }, + }, + args: args{ + ctx: context.Background(), + member: &domain.Member{ + ObjectRoot: models.ObjectRoot{ + AggregateID: "org1", + }, + UserID: "user1", + Roles: []string{"ORG_OWNER"}, + }, + }, + res: res{ + err: caos_errs.IsErrorAlreadyExists, + }, + }, + { + name: "member add uniqueconstraint err, already exists", + fields: fields{ + eventstore: eventstoreExpect( + t, + expectFilter( + eventFromEventPusher( + user.NewHumanAddedEvent(context.Background(), + &user.NewAggregate("user1", "org1").Aggregate, + "username1", + "firstname1", + "lastname1", + "nickname1", + "displayname1", + language.German, + domain.GenderMale, + "email1", + true, + ), + ), + ), + expectFilter(), + expectPushFailed(caos_errs.ThrowAlreadyExists(nil, "ERROR", "internal"), + []*repository.Event{ + eventFromEventPusher(org.NewMemberAddedEvent(context.Background(), + &org.NewAggregate("org1", "org1").Aggregate, + "user1", + []string{"ORG_OWNER"}..., + )), + }, + uniqueConstraintsFromEventConstraint(member.NewAddMemberUniqueConstraint("org1", "user1")), + ), + ), + zitadelRoles: []authz.RoleMapping{ + { + Role: domain.RoleOrgOwner, + }, + }, + }, + args: args{ + ctx: context.Background(), + member: &domain.Member{ + ObjectRoot: models.ObjectRoot{ + AggregateID: "org1", + }, + UserID: "user1", + Roles: []string{"ORG_OWNER"}, + }, + }, + res: res{ + err: caos_errs.IsErrorAlreadyExists, + }, + }, + { + name: "member add, ok", + fields: fields{ + eventstore: eventstoreExpect( + t, + expectFilter( + eventFromEventPusher( + user.NewHumanAddedEvent(context.Background(), + &user.NewAggregate("user1", "org1").Aggregate, + "username1", + "firstname1", + "lastname1", + "nickname1", + "displayname1", + language.German, + domain.GenderMale, + "email1", + true, + ), + ), + ), + expectFilter(), + expectPush( + []*repository.Event{ + eventFromEventPusher(org.NewMemberAddedEvent(context.Background(), + &org.NewAggregate("org1", "org1").Aggregate, + "user1", + []string{"ORG_OWNER"}..., + )), + }, + uniqueConstraintsFromEventConstraint(member.NewAddMemberUniqueConstraint("org1", "user1")), + ), + ), + zitadelRoles: []authz.RoleMapping{ + { + Role: domain.RoleOrgOwner, + }, + }, + }, + args: args{ + ctx: context.Background(), + member: &domain.Member{ + ObjectRoot: models.ObjectRoot{ + AggregateID: "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}, + }, + }, + }, + } + 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.AddOrgMember(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_ChangeOrgMember(t *testing.T) { + type fields struct { + eventstore *eventstore.Eventstore + zitadelRoles []authz.RoleMapping + } + type args struct { + ctx context.Context + member *domain.Member + } + type res struct { + want *domain.Member + err func(error) bool + } + tests := []struct { + name string + fields fields + args args + res res + }{ + { + name: "invalid member, error", + fields: fields{ + eventstore: eventstoreExpect( + t, + ), + }, + args: args{ + ctx: context.Background(), + member: &domain.Member{ + ObjectRoot: models.ObjectRoot{ + AggregateID: "org1", + }, + }, + }, + res: res{ + err: caos_errs.IsErrorInvalidArgument, + }, + }, + { + name: "invalid roles, error", + fields: fields{ + eventstore: eventstoreExpect( + t, + ), + }, + args: args{ + ctx: context.Background(), + member: &domain.Member{ + ObjectRoot: models.ObjectRoot{ + AggregateID: "org1", + }, + UserID: "user1", + Roles: []string{"PROJECT_OWNER"}, + }, + }, + res: res{ + err: caos_errs.IsErrorInvalidArgument, + }, + }, + { + name: "member not existing, not found error", + fields: fields{ + eventstore: eventstoreExpect( + t, + expectFilter(), + ), + zitadelRoles: []authz.RoleMapping{ + { + Role: domain.RoleOrgOwner, + }, + }, + }, + args: args{ + ctx: context.Background(), + member: &domain.Member{ + ObjectRoot: models.ObjectRoot{ + AggregateID: "org1", + }, + UserID: "user1", + Roles: []string{"ORG_OWNER"}, + }, + }, + res: res{ + err: caos_errs.IsNotFound, + }, + }, + { + name: "member not changed, precondition error", + fields: fields{ + eventstore: eventstoreExpect( + t, + expectFilter( + eventFromEventPusher( + org.NewMemberAddedEvent(context.Background(), + &org.NewAggregate("org1", "org1").Aggregate, + "user1", + []string{"ORG_OWNER"}..., + ), + ), + ), + ), + zitadelRoles: []authz.RoleMapping{ + { + Role: domain.RoleOrgOwner, + }, + }, + }, + args: args{ + ctx: context.Background(), + member: &domain.Member{ + ObjectRoot: models.ObjectRoot{ + AggregateID: "org1", + }, + UserID: "user1", + Roles: []string{"ORG_OWNER"}, + }, + }, + res: res{ + err: caos_errs.IsPreconditionFailed, + }, + }, + { + name: "member change, ok", + fields: fields{ + eventstore: eventstoreExpect( + t, + expectFilter( + eventFromEventPusher( + org.NewMemberAddedEvent(context.Background(), + &org.NewAggregate("org1", "org1").Aggregate, + "user1", + []string{"ORG_OWNER"}..., + ), + ), + ), + expectPush( + []*repository.Event{ + eventFromEventPusher(org.NewMemberChangedEvent(context.Background(), + &org.NewAggregate("org1", "org1").Aggregate, + "user1", + []string{"ORG_OWNER", "ORG_OWNER_VIEWER"}..., + )), + }, + ), + ), + zitadelRoles: []authz.RoleMapping{ + { + Role: "ORG_OWNER", + }, + { + Role: "ORG_OWNER_VIEWER", + }, + }, + }, + args: args{ + ctx: context.Background(), + member: &domain.Member{ + ObjectRoot: models.ObjectRoot{ + AggregateID: "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: caos_errs.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: caos_errs.IsErrorInvalidArgument, + }, + }, + { + name: "member not existing, nil result", + fields: fields{ + eventstore: eventstoreExpect( + t, + expectFilter(), + ), + }, + args: args{ + ctx: context.Background(), + projectID: "project1", + userID: "user1", + resourceOwner: "org1", + }, + res: res{ + want: nil, + }, + }, + { + 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( + []*repository.Event{ + eventFromEventPusher(project.NewProjectMemberRemovedEvent(context.Background(), + &project.NewAggregate("project1", "org1").Aggregate, + "user1", + )), + }, + uniqueConstraintsFromEventConstraint(member.NewRemoveMemberUniqueConstraint("project1", "user1")), + ), + ), + }, + args: args{ + ctx: context.Background(), + projectID: "project1", + userID: "user1", + resourceOwner: "org1", + }, + res: res{ + want: &domain.ObjectDetails{ + ResourceOwner: "org1", + }, + }, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + r := &Commands{ + eventstore: tt.fields.eventstore, + } + got, err := r.RemoveProjectMember(tt.args.ctx, tt.args.projectID, tt.args.userID, tt.args.resourceOwner) + 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) + } + }) + } +} diff --git a/internal/command/org_model.go b/internal/command/org_model.go index d0714a537..b95e792a1 100644 --- a/internal/command/org_model.go +++ b/internal/command/org_model.go @@ -29,6 +29,8 @@ func (wm *OrgWriteModel) Reduce() error { case *org.OrgAddedEvent: wm.Name = e.Name wm.State = domain.OrgStateActive + case *org.OrgDeactivatedEvent: + wm.State = domain.OrgStateInactive case *org.OrgChangedEvent: wm.Name = e.Name case *org.DomainPrimarySetEvent: diff --git a/internal/command/org_test.go b/internal/command/org_test.go new file mode 100644 index 000000000..0952ed0c1 --- /dev/null +++ b/internal/command/org_test.go @@ -0,0 +1,663 @@ +package command + +import ( + "context" + "github.com/caos/zitadel/internal/api/authz" + "github.com/caos/zitadel/internal/domain" + caos_errs "github.com/caos/zitadel/internal/errors" + "github.com/caos/zitadel/internal/eventstore" + "github.com/caos/zitadel/internal/eventstore/repository" + "github.com/caos/zitadel/internal/eventstore/v1/models" + "github.com/caos/zitadel/internal/id" + id_mock "github.com/caos/zitadel/internal/id/mock" + "github.com/caos/zitadel/internal/repository/member" + "github.com/caos/zitadel/internal/repository/org" + "github.com/caos/zitadel/internal/repository/user" + "github.com/stretchr/testify/assert" + "golang.org/x/text/language" + "testing" +) + +func TestCommandSide_AddOrg(t *testing.T) { + type fields struct { + eventstore *eventstore.Eventstore + idGenerator id.Generator + iamDomain string + zitadelRoles []authz.RoleMapping + } + type args struct { + ctx context.Context + name string + userID string + resourceOwner string + } + type res struct { + want *domain.Org + err func(error) bool + } + tests := []struct { + name string + fields fields + args args + res res + }{ + { + name: "invalid org, error", + fields: fields{ + eventstore: eventstoreExpect( + t, + ), + }, + args: args{ + ctx: context.Background(), + userID: "user1", + resourceOwner: "org1", + }, + res: res{ + err: caos_errs.IsErrorInvalidArgument, + }, + }, + { + name: "user removed, error", + fields: fields{ + eventstore: eventstoreExpect( + t, + expectFilterOrgDomainNotFound(), + expectFilter( + eventFromEventPusher( + user.NewHumanAddedEvent(context.Background(), + &user.NewAggregate("user1", "org1").Aggregate, + "username1", + "firstname1", + "lastname1", + "nickname1", + "displayname1", + language.German, + domain.GenderMale, + "email1", + true, + ), + ), + eventFromEventPusher( + user.NewUserRemovedEvent( + context.Background(), + &user.NewAggregate("user1", "org1").Aggregate, + "username1", + true, + ), + ), + ), + ), + idGenerator: id_mock.NewIDGeneratorExpectIDs(t, "org2"), + }, + args: args{ + ctx: context.Background(), + name: "Org", + userID: "user1", + resourceOwner: "org1", + }, + res: res{ + err: caos_errs.IsPreconditionFailed, + }, + }, + { + name: "push failed unique constraint, error", + fields: fields{ + eventstore: eventstoreExpect( + t, + expectFilterOrgDomainNotFound(), + expectFilter( + eventFromEventPusher( + user.NewHumanAddedEvent(context.Background(), + &user.NewAggregate("user1", "org1").Aggregate, + "username1", + "firstname1", + "lastname1", + "nickname1", + "displayname1", + language.German, + domain.GenderMale, + "email1", + true, + ), + ), + ), + expectFilter( + eventFromEventPusher( + user.NewHumanAddedEvent(context.Background(), + &user.NewAggregate("user1", "org1").Aggregate, + "username1", + "firstname1", + "lastname1", + "nickname1", + "displayname1", + language.German, + domain.GenderMale, + "email1", + true, + ), + ), + ), + expectFilterOrgMemberNotFound(), + expectPushFailed(caos_errs.ThrowAlreadyExists(nil, "id", "internal"), + []*repository.Event{ + eventFromEventPusher(org.NewOrgAddedEvent( + context.Background(), + &org.NewAggregate("org2", "org2").Aggregate, + "Org")), + eventFromEventPusher(org.NewDomainAddedEvent( + context.Background(), + &org.NewAggregate("org2", "org2").Aggregate, + "org.iam-domain")), + eventFromEventPusher(org.NewDomainVerifiedEvent( + context.Background(), + &org.NewAggregate("org2", "org2").Aggregate, + "org.iam-domain")), + eventFromEventPusher(org.NewDomainPrimarySetEvent( + context.Background(), + &org.NewAggregate("org2", "org2").Aggregate, + "org.iam-domain")), + eventFromEventPusher(org.NewMemberAddedEvent( + context.Background(), + &org.NewAggregate("org2", "org2").Aggregate, + "user1", domain.RoleOrgOwner)), + }, + uniqueConstraintsFromEventConstraint(org.NewAddOrgNameUniqueConstraint("Org")), + uniqueConstraintsFromEventConstraint(org.NewAddOrgDomainUniqueConstraint("org.iam-domain")), + uniqueConstraintsFromEventConstraint(member.NewAddMemberUniqueConstraint("org2", "user1")), + ), + ), + idGenerator: id_mock.NewIDGeneratorExpectIDs(t, "org2"), + iamDomain: "iam-domain", + zitadelRoles: []authz.RoleMapping{ + { + Role: "ORG_OWNER", + }, + }, + }, + args: args{ + ctx: context.Background(), + name: "Org", + userID: "user1", + resourceOwner: "org1", + }, + res: res{ + err: caos_errs.IsErrorAlreadyExists, + }, + }, + { + name: "push failed, error", + fields: fields{ + eventstore: eventstoreExpect( + t, + expectFilterOrgDomainNotFound(), + expectFilter( + eventFromEventPusher( + user.NewHumanAddedEvent(context.Background(), + &user.NewAggregate("user1", "org1").Aggregate, + "username1", + "firstname1", + "lastname1", + "nickname1", + "displayname1", + language.German, + domain.GenderMale, + "email1", + true, + ), + ), + ), + expectFilter( + eventFromEventPusher( + user.NewHumanAddedEvent(context.Background(), + &user.NewAggregate("user1", "org1").Aggregate, + "username1", + "firstname1", + "lastname1", + "nickname1", + "displayname1", + language.German, + domain.GenderMale, + "email1", + true, + ), + ), + ), + expectFilterOrgMemberNotFound(), + expectPushFailed(caos_errs.ThrowInternal(nil, "id", "internal"), + []*repository.Event{ + eventFromEventPusher(org.NewOrgAddedEvent( + context.Background(), + &org.NewAggregate("org2", "org2").Aggregate, + "Org")), + eventFromEventPusher(org.NewDomainAddedEvent( + context.Background(), + &org.NewAggregate("org2", "org2").Aggregate, + "org.iam-domain")), + eventFromEventPusher(org.NewDomainVerifiedEvent( + context.Background(), + &org.NewAggregate("org2", "org2").Aggregate, + "org.iam-domain")), + eventFromEventPusher(org.NewDomainPrimarySetEvent( + context.Background(), + &org.NewAggregate("org2", "org2").Aggregate, + "org.iam-domain")), + eventFromEventPusher(org.NewMemberAddedEvent( + context.Background(), + &org.NewAggregate("org2", "org2").Aggregate, + "user1", domain.RoleOrgOwner)), + }, + uniqueConstraintsFromEventConstraint(org.NewAddOrgNameUniqueConstraint("Org")), + uniqueConstraintsFromEventConstraint(org.NewAddOrgDomainUniqueConstraint("org.iam-domain")), + uniqueConstraintsFromEventConstraint(member.NewAddMemberUniqueConstraint("org2", "user1")), + ), + ), + idGenerator: id_mock.NewIDGeneratorExpectIDs(t, "org2"), + iamDomain: "iam-domain", + zitadelRoles: []authz.RoleMapping{ + { + Role: "ORG_OWNER", + }, + }, + }, + args: args{ + ctx: context.Background(), + name: "Org", + userID: "user1", + resourceOwner: "org1", + }, + res: res{ + err: caos_errs.IsInternal, + }, + }, + { + name: "add org, no error", + fields: fields{ + eventstore: eventstoreExpect( + t, + expectFilterOrgDomainNotFound(), + expectFilter( + eventFromEventPusher( + user.NewHumanAddedEvent(context.Background(), + &user.NewAggregate("user1", "org1").Aggregate, + "username1", + "firstname1", + "lastname1", + "nickname1", + "displayname1", + language.German, + domain.GenderMale, + "email1", + true, + ), + ), + ), + expectFilter( + eventFromEventPusher( + user.NewHumanAddedEvent(context.Background(), + &user.NewAggregate("user1", "org1").Aggregate, + "username1", + "firstname1", + "lastname1", + "nickname1", + "displayname1", + language.German, + domain.GenderMale, + "email1", + true, + ), + ), + ), + expectFilterOrgMemberNotFound(), + expectPush( + []*repository.Event{ + eventFromEventPusher(org.NewOrgAddedEvent(context.Background(), + &org.NewAggregate("org2", "org2").Aggregate, + "Org", + )), + eventFromEventPusher(org.NewDomainAddedEvent(context.Background(), + &org.NewAggregate("org2", "org2").Aggregate, "org.iam-domain", + )), + eventFromEventPusher(org.NewDomainVerifiedEvent(context.Background(), + &org.NewAggregate("org2", "org2").Aggregate, + "org.iam-domain", + )), + eventFromEventPusher(org.NewDomainPrimarySetEvent(context.Background(), + &org.NewAggregate("org2", "org2").Aggregate, + "org.iam-domain", + )), + eventFromEventPusher(org.NewMemberAddedEvent(context.Background(), + &org.NewAggregate("org2", "org2").Aggregate, + "user1", + domain.RoleOrgOwner, + )), + }, + uniqueConstraintsFromEventConstraint(org.NewAddOrgNameUniqueConstraint("Org")), + uniqueConstraintsFromEventConstraint(org.NewAddOrgDomainUniqueConstraint("org.iam-domain")), + uniqueConstraintsFromEventConstraint(member.NewAddMemberUniqueConstraint("org2", "user1")), + ), + ), + idGenerator: id_mock.NewIDGeneratorExpectIDs(t, "org2"), + iamDomain: "iam-domain", + zitadelRoles: []authz.RoleMapping{ + { + Role: "ORG_OWNER", + }, + }, + }, + args: args{ + ctx: context.Background(), + name: "Org", + userID: "user1", + resourceOwner: "org1", + }, + res: res{ + want: &domain.Org{ + ObjectRoot: models.ObjectRoot{ + AggregateID: "org2", + ResourceOwner: "org2", + }, + Name: "Org", + State: domain.OrgStateActive, + PrimaryDomain: "org.iam-domain", + }, + }, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + r := &Commands{ + eventstore: tt.fields.eventstore, + idGenerator: tt.fields.idGenerator, + iamDomain: tt.fields.iamDomain, + zitadelRoles: tt.fields.zitadelRoles, + } + got, err := r.AddOrg(tt.args.ctx, tt.args.name, tt.args.userID, tt.args.resourceOwner) + 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_DeactivateOrg(t *testing.T) { + type fields struct { + eventstore *eventstore.Eventstore + idGenerator id.Generator + iamDomain string + } + type args struct { + ctx context.Context + orgID string + } + type res struct { + want *domain.Org + err func(error) bool + } + tests := []struct { + name string + fields fields + args args + res res + }{ + { + name: "org not found, error", + fields: fields{ + eventstore: eventstoreExpect( + t, + expectFilter(), + ), + }, + args: args{ + ctx: context.Background(), + orgID: "org1", + }, + res: res{ + err: caos_errs.IsNotFound, + }, + }, + { + name: "org already inactive, error", + fields: fields{ + eventstore: eventstoreExpect( + t, + expectFilter( + eventFromEventPusher( + org.NewOrgAddedEvent(context.Background(), + &org.NewAggregate("org1", "org1").Aggregate, + "org"), + ), + eventFromEventPusher( + org.NewOrgDeactivatedEvent(context.Background(), + &org.NewAggregate("org1", "org1").Aggregate), + ), + ), + ), + }, + args: args{ + ctx: context.Background(), + orgID: "org1", + }, + res: res{ + err: caos_errs.IsPreconditionFailed, + }, + }, + { + name: "push failed, error", + fields: fields{ + eventstore: eventstoreExpect( + t, + expectFilter( + eventFromEventPusher( + org.NewOrgAddedEvent(context.Background(), + &org.NewAggregate("org1", "org1").Aggregate, + "org"), + ), + ), + expectPushFailed( + caos_errs.ThrowInternal(nil, "id", "message"), + []*repository.Event{ + eventFromEventPusher(org.NewOrgDeactivatedEvent(context.Background(), + &org.NewAggregate("org1", "org1").Aggregate)), + }, + ), + ), + }, + args: args{ + ctx: context.Background(), + orgID: "org1", + }, + res: res{ + err: caos_errs.IsInternal, + }, + }, + { + name: "deactivate org", + fields: fields{ + eventstore: eventstoreExpect( + t, + expectFilter( + eventFromEventPusher( + org.NewOrgAddedEvent(context.Background(), + &org.NewAggregate("org1", "org1").Aggregate, + "org"), + ), + ), + expectPush( + []*repository.Event{ + eventFromEventPusher(org.NewOrgDeactivatedEvent(context.Background(), + &org.NewAggregate("org1", "org1").Aggregate, + )), + }, + ), + ), + }, + args: args{ + ctx: context.Background(), + orgID: "org1", + }, + res: res{}, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + r := &Commands{ + eventstore: tt.fields.eventstore, + idGenerator: tt.fields.idGenerator, + } + _, err := r.DeactivateOrg(tt.args.ctx, tt.args.orgID) + 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) + } + }) + } +} +func TestCommandSide_ReactivateOrg(t *testing.T) { + type fields struct { + eventstore *eventstore.Eventstore + idGenerator id.Generator + iamDomain string + } + type args struct { + ctx context.Context + orgID string + } + type res struct { + want *domain.Org + err func(error) bool + } + tests := []struct { + name string + fields fields + args args + res res + }{ + { + name: "org not found, error", + fields: fields{ + eventstore: eventstoreExpect( + t, + expectFilter(), + ), + }, + args: args{ + ctx: context.Background(), + orgID: "org1", + }, + res: res{ + err: caos_errs.IsNotFound, + }, + }, + { + name: "org already active, error", + fields: fields{ + eventstore: eventstoreExpect( + t, + expectFilter( + eventFromEventPusher( + org.NewOrgAddedEvent(context.Background(), + &org.NewAggregate("org1", "org1").Aggregate, + "org"), + ), + ), + ), + }, + args: args{ + ctx: context.Background(), + orgID: "org1", + }, + res: res{ + err: caos_errs.IsPreconditionFailed, + }, + }, + { + name: "push failed, error", + fields: fields{ + eventstore: eventstoreExpect( + t, + expectFilter( + eventFromEventPusher( + org.NewOrgAddedEvent(context.Background(), + &org.NewAggregate("org1", "org1").Aggregate, + "org"), + ), + eventFromEventPusher( + org.NewOrgDeactivatedEvent(context.Background(), + &org.NewAggregate("org1", "org1").Aggregate, + ), + ), + ), + expectPushFailed( + caos_errs.ThrowInternal(nil, "id", "message"), + []*repository.Event{ + eventFromEventPusher(org.NewOrgReactivatedEvent(context.Background(), + &org.NewAggregate("org1", "org1").Aggregate, + )), + }, + ), + ), + }, + args: args{ + ctx: context.Background(), + orgID: "org1", + }, + res: res{ + err: caos_errs.IsInternal, + }, + }, + { + name: "reactivate org", + fields: fields{ + eventstore: eventstoreExpect( + t, + expectFilter( + eventFromEventPusher( + org.NewOrgAddedEvent(context.Background(), + &org.NewAggregate("org1", "org1").Aggregate, + "org"), + ), + eventFromEventPusher( + org.NewOrgDeactivatedEvent(context.Background(), + &org.NewAggregate("org1", "org1").Aggregate), + ), + ), + expectPush( + []*repository.Event{ + eventFromEventPusher(org.NewOrgReactivatedEvent(context.Background(), + &org.NewAggregate("org1", "org1").Aggregate)), + }, + ), + ), + }, + args: args{ + ctx: context.Background(), + orgID: "org1", + }, + res: res{}, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + r := &Commands{ + eventstore: tt.fields.eventstore, + idGenerator: tt.fields.idGenerator, + } + _, err := r.ReactivateOrg(tt.args.ctx, tt.args.orgID) + 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) + } + }) + } +} diff --git a/internal/command/project.go b/internal/command/project.go index 505d029bb..b5f3bbdd1 100644 --- a/internal/command/project.go +++ b/internal/command/project.go @@ -27,7 +27,7 @@ func (c *Commands) AddProject(ctx context.Context, project *domain.Project, reso func (c *Commands) addProject(ctx context.Context, projectAdd *domain.Project, resourceOwner, ownerUserID string) (_ []eventstore.EventPusher, _ *ProjectWriteModel, err error) { if !projectAdd.IsValid() { - return nil, nil, caos_errs.ThrowPreconditionFailed(nil, "PROJECT-IOVCC", "Errors.Project.Invalid") + return nil, nil, caos_errs.ThrowInvalidArgument(nil, "PROJECT-IOVCC", "Errors.Project.Invalid") } projectAdd.AggregateID, err = c.idGenerator.Next() if err != nil { @@ -74,8 +74,8 @@ func (c *Commands) checkProjectExists(ctx context.Context, projectID, resourceOw } func (c *Commands) ChangeProject(ctx context.Context, projectChange *domain.Project, resourceOwner string) (*domain.Project, error) { - if !projectChange.IsValid() && projectChange.AggregateID != "" { - return nil, caos_errs.ThrowPreconditionFailed(nil, "COMMAND-4m9vS", "Errors.Project.Invalid") + if !projectChange.IsValid() || projectChange.AggregateID == "" { + return nil, caos_errs.ThrowInvalidArgument(nil, "COMMAND-4m9vS", "Errors.Project.Invalid") } existingProject, err := c.getProjectWriteModelByID(ctx, projectChange.AggregateID, resourceOwner) @@ -107,7 +107,7 @@ func (c *Commands) ChangeProject(ctx context.Context, projectChange *domain.Proj func (c *Commands) DeactivateProject(ctx context.Context, projectID string, resourceOwner string) (*domain.ObjectDetails, error) { if projectID == "" || resourceOwner == "" { - return nil, caos_errs.ThrowPreconditionFailed(nil, "COMMAND-88iF0", "Errors.Project.ProjectIDMissing") + return nil, caos_errs.ThrowInvalidArgument(nil, "COMMAND-88iF0", "Errors.Project.ProjectIDMissing") } existingProject, err := c.getProjectWriteModelByID(ctx, projectID, resourceOwner) @@ -135,7 +135,7 @@ func (c *Commands) DeactivateProject(ctx context.Context, projectID string, reso func (c *Commands) ReactivateProject(ctx context.Context, projectID string, resourceOwner string) (*domain.ObjectDetails, error) { if projectID == "" || resourceOwner == "" { - return nil, caos_errs.ThrowPreconditionFailed(nil, "COMMAND-4m9vS", "Errors.Project.ProjectIDMissing") + return nil, caos_errs.ThrowInvalidArgument(nil, "COMMAND-4m9vS", "Errors.Project.ProjectIDMissing") } existingProject, err := c.getProjectWriteModelByID(ctx, projectID, resourceOwner) @@ -146,7 +146,7 @@ func (c *Commands) ReactivateProject(ctx context.Context, projectID string, reso return nil, caos_errs.ThrowNotFound(nil, "COMMAND-3M9sd", "Errors.Project.NotFound") } if existingProject.State != domain.ProjectStateInactive { - return nil, caos_errs.ThrowPreconditionFailed(nil, "COMMAND-5M9bs", "Errors.Project.NotInctive") + return nil, caos_errs.ThrowPreconditionFailed(nil, "COMMAND-5M9bs", "Errors.Project.NotInactive") } projectAgg := ProjectAggregateFromWriteModel(&existingProject.WriteModel) @@ -163,7 +163,7 @@ func (c *Commands) ReactivateProject(ctx context.Context, projectID string, reso func (c *Commands) RemoveProject(ctx context.Context, projectID, resourceOwner string, cascadingUserGrantIDs ...string) (*domain.ObjectDetails, error) { if projectID == "" || resourceOwner == "" { - return nil, caos_errs.ThrowPreconditionFailed(nil, "COMMAND-66hM9", "Errors.Project.ProjectIDMissing") + return nil, caos_errs.ThrowInvalidArgument(nil, "COMMAND-66hM9", "Errors.Project.ProjectIDMissing") } existingProject, err := c.getProjectWriteModelByID(ctx, projectID, resourceOwner) diff --git a/internal/command/project_application.go b/internal/command/project_application.go index 4b7ceacf1..957596efa 100644 --- a/internal/command/project_application.go +++ b/internal/command/project_application.go @@ -8,8 +8,8 @@ import ( ) func (c *Commands) ChangeApplication(ctx context.Context, projectID string, appChange domain.Application, resourceOwner string) (*domain.ObjectDetails, error) { - if appChange.GetAppID() == "" || appChange.GetApplicationName() == "" { - return nil, caos_errs.ThrowPreconditionFailed(nil, "COMMAND-4m9vS", "Errors.Project.App.Invalid") + if projectID == "" || appChange.GetAppID() == "" || appChange.GetApplicationName() == "" { + return nil, caos_errs.ThrowInvalidArgument(nil, "COMMAND-4m9vS", "Errors.Project.App.Invalid") } existingApp, err := c.getApplicationWriteModel(ctx, projectID, appChange.GetAppID(), resourceOwner) @@ -25,7 +25,7 @@ func (c *Commands) ChangeApplication(ctx context.Context, projectID string, appC projectAgg := ProjectAggregateFromWriteModel(&existingApp.WriteModel) pushedEvents, err := c.eventstore.PushEvents( ctx, - project.NewApplicationChangedEvent(ctx, projectAgg, appChange.GetAppID(), existingApp.Name, appChange.GetApplicationName(), projectID)) + project.NewApplicationChangedEvent(ctx, projectAgg, appChange.GetAppID(), existingApp.Name, appChange.GetApplicationName())) if err != nil { return nil, err } @@ -38,7 +38,7 @@ func (c *Commands) ChangeApplication(ctx context.Context, projectID string, appC func (c *Commands) DeactivateApplication(ctx context.Context, projectID, appID, resourceOwner string) (*domain.ObjectDetails, error) { if projectID == "" || appID == "" { - return nil, caos_errs.ThrowPreconditionFailed(nil, "COMMAND-88fi0", "Errors.IDMissing") + return nil, caos_errs.ThrowInvalidArgument(nil, "COMMAND-88fi0", "Errors.IDMissing") } existingApp, err := c.getApplicationWriteModel(ctx, projectID, appID, resourceOwner) @@ -65,7 +65,7 @@ func (c *Commands) DeactivateApplication(ctx context.Context, projectID, appID, func (c *Commands) ReactivateApplication(ctx context.Context, projectID, appID, resourceOwner string) (*domain.ObjectDetails, error) { if projectID == "" || appID == "" { - return nil, caos_errs.ThrowPreconditionFailed(nil, "COMMAND-983dF", "Errors.IDMissing") + return nil, caos_errs.ThrowInvalidArgument(nil, "COMMAND-983dF", "Errors.IDMissing") } existingApp, err := c.getApplicationWriteModel(ctx, projectID, appID, resourceOwner) @@ -93,7 +93,7 @@ func (c *Commands) ReactivateApplication(ctx context.Context, projectID, appID, func (c *Commands) RemoveApplication(ctx context.Context, projectID, appID, resourceOwner string) (*domain.ObjectDetails, error) { if projectID == "" || appID == "" { - return nil, caos_errs.ThrowPreconditionFailed(nil, "COMMAND-1b7Jf", "Errors.IDMissing") + return nil, caos_errs.ThrowInvalidArgument(nil, "COMMAND-1b7Jf", "Errors.IDMissing") } existingApp, err := c.getApplicationWriteModel(ctx, projectID, appID, resourceOwner) @@ -105,7 +105,7 @@ func (c *Commands) RemoveApplication(ctx context.Context, projectID, appID, reso } projectAgg := ProjectAggregateFromWriteModel(&existingApp.WriteModel) - pushedEvents, err := c.eventstore.PushEvents(ctx, project.NewApplicationRemovedEvent(ctx, projectAgg, appID, existingApp.Name, projectID)) + pushedEvents, err := c.eventstore.PushEvents(ctx, project.NewApplicationRemovedEvent(ctx, projectAgg, appID, existingApp.Name)) if err != nil { return nil, err } diff --git a/internal/command/project_application_api.go b/internal/command/project_application_api.go index a67273daf..162daf06d 100644 --- a/internal/command/project_application_api.go +++ b/internal/command/project_application_api.go @@ -9,9 +9,12 @@ import ( ) func (c *Commands) AddAPIApplication(ctx context.Context, application *domain.APIApp, resourceOwner string) (_ *domain.APIApp, err error) { + if application == nil || application.AggregateID == "" { + return nil, caos_errs.ThrowInvalidArgument(nil, "PROJECT-5m9E", "Errors.Application.Invalid") + } project, err := c.getProjectByID(ctx, application.AggregateID, resourceOwner) if err != nil { - return nil, err + return nil, caos_errs.ThrowPreconditionFailed(err, "PROJECT-9fnsf", "Errors.Project.NotFound") } addedApplication := NewAPIApplicationWriteModel(application.AggregateID, resourceOwner) projectAgg := ProjectAggregateFromWriteModel(&addedApplication.WriteModel) @@ -35,7 +38,7 @@ func (c *Commands) AddAPIApplication(ctx context.Context, application *domain.AP func (c *Commands) addAPIApplication(ctx context.Context, projectAgg *eventstore.Aggregate, proj *domain.Project, apiAppApp *domain.APIApp, resourceOwner string) (events []eventstore.EventPusher, stringPW string, err error) { if !apiAppApp.IsValid() { - return nil, "", caos_errs.ThrowPreconditionFailed(nil, "PROJECT-Bff2g", "Errors.Application.Invalid") + return nil, "", caos_errs.ThrowInvalidArgument(nil, "PROJECT-Bff2g", "Errors.Application.Invalid") } apiAppApp.AppID, err = c.idGenerator.Next() if err != nil { @@ -43,7 +46,7 @@ func (c *Commands) addAPIApplication(ctx context.Context, projectAgg *eventstore } events = []eventstore.EventPusher{ - project.NewApplicationAddedEvent(ctx, projectAgg, apiAppApp.AppID, apiAppApp.AppName, resourceOwner), + project.NewApplicationAddedEvent(ctx, projectAgg, apiAppApp.AppID, apiAppApp.AppName), } var stringPw string @@ -66,8 +69,8 @@ func (c *Commands) addAPIApplication(ctx context.Context, projectAgg *eventstore } func (c *Commands) ChangeAPIApplication(ctx context.Context, apiApp *domain.APIApp, resourceOwner string) (*domain.APIApp, error) { - if !apiApp.IsValid() { - return nil, caos_errs.ThrowPreconditionFailed(nil, "COMMAND-1m900", "Errors.Project.App.APIConfigInvalid") + if !apiApp.IsValid() || apiApp.AppID == "" || apiApp.AggregateID == "" { + return nil, caos_errs.ThrowInvalidArgument(nil, "COMMAND-1m900", "Errors.Project.App.APIConfigInvalid") } existingAPI, err := c.getAPIAppWriteModel(ctx, apiApp.AggregateID, apiApp.AppID, resourceOwner) @@ -99,13 +102,12 @@ func (c *Commands) ChangeAPIApplication(ctx context.Context, apiApp *domain.APIA return nil, err } - result := apiWriteModelToAPIConfig(existingAPI) - return result, nil + return apiWriteModelToAPIConfig(existingAPI), nil } func (c *Commands) ChangeAPIApplicationSecret(ctx context.Context, projectID, appID, resourceOwner string) (*domain.APIApp, error) { if projectID == "" || appID == "" { - return nil, caos_errs.ThrowPreconditionFailed(nil, "COMMAND-99i83", "Errors.IDMissing") + return nil, caos_errs.ThrowInvalidArgument(nil, "COMMAND-99i83", "Errors.IDMissing") } existingAPI, err := c.getAPIAppWriteModel(ctx, projectID, appID, resourceOwner) diff --git a/internal/command/project_application_api_test.go b/internal/command/project_application_api_test.go new file mode 100644 index 000000000..d98930695 --- /dev/null +++ b/internal/command/project_application_api_test.go @@ -0,0 +1,660 @@ +package command + +import ( + "context" + "github.com/caos/zitadel/internal/crypto" + "github.com/caos/zitadel/internal/domain" + caos_errs "github.com/caos/zitadel/internal/errors" + "github.com/caos/zitadel/internal/eventstore" + "github.com/caos/zitadel/internal/eventstore/repository" + "github.com/caos/zitadel/internal/eventstore/v1/models" + "github.com/caos/zitadel/internal/id" + id_mock "github.com/caos/zitadel/internal/id/mock" + "github.com/caos/zitadel/internal/repository/project" + "github.com/stretchr/testify/assert" + "testing" +) + +func TestCommandSide_AddAPIApplication(t *testing.T) { + type fields struct { + eventstore *eventstore.Eventstore + idGenerator id.Generator + secretGenerator crypto.Generator + } + type args struct { + ctx context.Context + apiApp *domain.APIApp + resourceOwner string + } + type res struct { + want *domain.APIApp + err func(error) bool + } + tests := []struct { + name string + fields fields + args args + res res + }{ + { + name: "no aggregate id, invalid argument error", + fields: fields{ + eventstore: eventstoreExpect( + t, + ), + }, + args: args{ + ctx: context.Background(), + apiApp: &domain.APIApp{}, + resourceOwner: "org1", + }, + res: res{ + err: caos_errs.IsErrorInvalidArgument, + }, + }, + { + name: "project not existing, not found error", + fields: fields{ + eventstore: eventstoreExpect( + t, + expectFilter(), + ), + }, + args: args{ + ctx: context.Background(), + apiApp: &domain.APIApp{ + ObjectRoot: models.ObjectRoot{ + AggregateID: "project1", + }, + AppID: "app1", + AppName: "app", + }, + resourceOwner: "org1", + }, + res: res{ + err: caos_errs.IsPreconditionFailed, + }, + }, + { + name: "invalid app, invalid argument error", + fields: fields{ + eventstore: eventstoreExpect( + t, + expectFilter( + eventFromEventPusher( + project.NewProjectAddedEvent(context.Background(), + &project.NewAggregate("project1", "org1").Aggregate, + "project"), + ), + ), + ), + }, + args: args{ + ctx: context.Background(), + apiApp: &domain.APIApp{ + ObjectRoot: models.ObjectRoot{ + AggregateID: "project1", + }, + AppID: "app1", + AppName: "", + }, + resourceOwner: "org1", + }, + res: res{ + err: caos_errs.IsErrorInvalidArgument, + }, + }, + { + name: "create api app basic, ok", + fields: fields{ + eventstore: eventstoreExpect( + t, + expectFilter( + eventFromEventPusher( + project.NewProjectAddedEvent(context.Background(), + &project.NewAggregate("project1", "org1").Aggregate, + "project"), + ), + ), + expectPush( + []*repository.Event{ + eventFromEventPusher( + project.NewApplicationAddedEvent(context.Background(), + &project.NewAggregate("project1", "org1").Aggregate, + "app1", + "app", + ), + ), + eventFromEventPusher( + project.NewAPIConfigAddedEvent(context.Background(), + &project.NewAggregate("project1", "org1").Aggregate, + "app1", + "client1@project", + &crypto.CryptoValue{ + CryptoType: crypto.TypeEncryption, + Algorithm: "enc", + KeyID: "id", + Crypted: []byte("a"), + }, + domain.APIAuthMethodTypeBasic), + ), + }, + uniqueConstraintsFromEventConstraint(project.NewAddApplicationUniqueConstraint("app", "project1")), + ), + ), + idGenerator: id_mock.NewIDGeneratorExpectIDs(t, "app1", "client1"), + secretGenerator: GetMockSecretGenerator(t), + }, + args: args{ + ctx: context.Background(), + apiApp: &domain.APIApp{ + ObjectRoot: models.ObjectRoot{ + AggregateID: "project1", + }, + AppName: "app", + AuthMethodType: domain.APIAuthMethodTypeBasic, + }, + resourceOwner: "org1", + }, + res: res{ + want: &domain.APIApp{ + ObjectRoot: models.ObjectRoot{ + AggregateID: "project1", + ResourceOwner: "org1", + }, + AppID: "app1", + AppName: "app", + ClientID: "client1@project", + ClientSecretString: "a", + AuthMethodType: domain.APIAuthMethodTypeBasic, + State: domain.AppStateActive, + }, + }, + }, + { + name: "create api app jwt, ok", + fields: fields{ + eventstore: eventstoreExpect( + t, + expectFilter( + eventFromEventPusher( + project.NewProjectAddedEvent(context.Background(), + &project.NewAggregate("project1", "org1").Aggregate, + "project"), + ), + ), + expectPush( + []*repository.Event{ + eventFromEventPusher( + project.NewApplicationAddedEvent(context.Background(), + &project.NewAggregate("project1", "org1").Aggregate, + "app1", + "app", + ), + ), + eventFromEventPusher( + project.NewAPIConfigAddedEvent(context.Background(), + &project.NewAggregate("project1", "org1").Aggregate, + "app1", + "client1@project", + nil, + domain.APIAuthMethodTypePrivateKeyJWT), + ), + }, + uniqueConstraintsFromEventConstraint(project.NewAddApplicationUniqueConstraint("app", "project1")), + ), + ), + idGenerator: id_mock.NewIDGeneratorExpectIDs(t, "app1", "client1"), + }, + args: args{ + ctx: context.Background(), + apiApp: &domain.APIApp{ + ObjectRoot: models.ObjectRoot{ + AggregateID: "project1", + }, + AppName: "app", + AuthMethodType: domain.APIAuthMethodTypePrivateKeyJWT, + }, + resourceOwner: "org1", + }, + res: res{ + want: &domain.APIApp{ + ObjectRoot: models.ObjectRoot{ + AggregateID: "project1", + ResourceOwner: "org1", + }, + AppID: "app1", + AppName: "app", + ClientID: "client1@project", + AuthMethodType: domain.APIAuthMethodTypePrivateKeyJWT, + State: domain.AppStateActive, + }, + }, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + r := &Commands{ + eventstore: tt.fields.eventstore, + idGenerator: tt.fields.idGenerator, + applicationSecretGenerator: tt.fields.secretGenerator, + } + got, err := r.AddAPIApplication(tt.args.ctx, tt.args.apiApp, tt.args.resourceOwner) + 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_ChangeAPIApplication(t *testing.T) { + type fields struct { + eventstore *eventstore.Eventstore + } + type args struct { + ctx context.Context + apiApp *domain.APIApp + resourceOwner string + } + type res struct { + want *domain.APIApp + err func(error) bool + } + tests := []struct { + name string + fields fields + args args + res res + }{ + { + name: "invalid app, invalid argument error", + fields: fields{ + eventstore: eventstoreExpect( + t, + ), + }, + args: args{ + ctx: context.Background(), + apiApp: &domain.APIApp{ + ObjectRoot: models.ObjectRoot{ + AggregateID: "project1", + }, + AppID: "app1", + }, + resourceOwner: "org1", + }, + res: res{ + err: caos_errs.IsErrorInvalidArgument, + }, + }, + { + name: "missing appid, invalid argument error", + fields: fields{ + eventstore: eventstoreExpect( + t, + ), + }, + args: args{ + ctx: context.Background(), + apiApp: &domain.APIApp{ + ObjectRoot: models.ObjectRoot{ + AggregateID: "project1", + }, + AppID: "", + AppName: "app", + AuthMethodType: domain.APIAuthMethodTypePrivateKeyJWT, + }, + resourceOwner: "org1", + }, + res: res{ + err: caos_errs.IsErrorInvalidArgument, + }, + }, + { + name: "missing aggregateid, invalid argument error", + fields: fields{ + eventstore: eventstoreExpect( + t, + ), + }, + args: args{ + ctx: context.Background(), + apiApp: &domain.APIApp{ + ObjectRoot: models.ObjectRoot{ + AggregateID: "", + }, + AppID: "appid", + AppName: "app", + AuthMethodType: domain.APIAuthMethodTypePrivateKeyJWT, + }, + resourceOwner: "org1", + }, + res: res{ + err: caos_errs.IsErrorInvalidArgument, + }, + }, + { + name: "app not existing, not found error", + fields: fields{ + eventstore: eventstoreExpect( + t, + expectFilter(), + ), + }, + args: args{ + ctx: context.Background(), + apiApp: &domain.APIApp{ + ObjectRoot: models.ObjectRoot{ + AggregateID: "project1", + }, + AppID: "app1", + AppName: "app", + }, + resourceOwner: "org1", + }, + res: res{ + err: caos_errs.IsNotFound, + }, + }, + { + name: "no changes, precondition error", + fields: fields{ + eventstore: eventstoreExpect( + t, + expectFilter( + eventFromEventPusher( + project.NewApplicationAddedEvent(context.Background(), + &project.NewAggregate("project1", "org1").Aggregate, + "app1", + "app", + ), + ), + eventFromEventPusher( + project.NewAPIConfigAddedEvent(context.Background(), + &project.NewAggregate("project1", "org1").Aggregate, + "app1", + "client1@project", + nil, + domain.APIAuthMethodTypePrivateKeyJWT), + ), + ), + ), + }, + args: args{ + ctx: context.Background(), + apiApp: &domain.APIApp{ + ObjectRoot: models.ObjectRoot{ + AggregateID: "project1", + }, + AppID: "app1", + AppName: "app", + AuthMethodType: domain.APIAuthMethodTypePrivateKeyJWT, + }, + resourceOwner: "org1", + }, + res: res{ + err: caos_errs.IsPreconditionFailed, + }, + }, + { + name: "change api app, ok", + fields: fields{ + eventstore: eventstoreExpect( + t, + expectFilter( + eventFromEventPusher( + project.NewApplicationAddedEvent(context.Background(), + &project.NewAggregate("project1", "org1").Aggregate, + "app1", + "app", + ), + ), + eventFromEventPusher( + project.NewAPIConfigAddedEvent(context.Background(), + &project.NewAggregate("project1", "org1").Aggregate, + "app1", + "client1@project", + &crypto.CryptoValue{ + CryptoType: crypto.TypeEncryption, + Algorithm: "enc", + KeyID: "id", + Crypted: []byte("a"), + }, + domain.APIAuthMethodTypeBasic), + ), + ), + expectPush( + []*repository.Event{ + eventFromEventPusher( + newAPIAppChangedEvent(context.Background(), + "app1", + "project1", + "org1", + domain.APIAuthMethodTypePrivateKeyJWT), + ), + }, + ), + ), + }, + args: args{ + ctx: context.Background(), + apiApp: &domain.APIApp{ + ObjectRoot: models.ObjectRoot{ + AggregateID: "project1", + }, + AppID: "app1", + AppName: "app", + AuthMethodType: domain.APIAuthMethodTypePrivateKeyJWT, + }, + resourceOwner: "org1", + }, + res: res{ + want: &domain.APIApp{ + ObjectRoot: models.ObjectRoot{ + AggregateID: "project1", + ResourceOwner: "org1", + }, + AppID: "app1", + AppName: "app", + ClientID: "client1@project", + AuthMethodType: domain.APIAuthMethodTypePrivateKeyJWT, + State: domain.AppStateActive, + }, + }, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + r := &Commands{ + eventstore: tt.fields.eventstore, + } + got, err := r.ChangeAPIApplication(tt.args.ctx, tt.args.apiApp, tt.args.resourceOwner) + 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_ChangeAPIApplicationSecret(t *testing.T) { + type fields struct { + eventstore *eventstore.Eventstore + secretGenerator crypto.Generator + } + type args struct { + ctx context.Context + appID string + projectID string + resourceOwner string + } + type res struct { + want *domain.APIApp + err func(error) bool + } + tests := []struct { + name string + fields fields + args args + res res + }{ + { + name: "no projectid, invalid argument error", + fields: fields{ + eventstore: eventstoreExpect( + t, + ), + }, + args: args{ + ctx: context.Background(), + appID: "app1", + resourceOwner: "org1", + }, + res: res{ + err: caos_errs.IsErrorInvalidArgument, + }, + }, + { + name: "no appid, invalid argument error", + fields: fields{ + eventstore: eventstoreExpect( + t, + ), + }, + args: args{ + ctx: context.Background(), + projectID: "project1", + appID: "", + resourceOwner: "org1", + }, + res: res{ + err: caos_errs.IsErrorInvalidArgument, + }, + }, + { + name: "app not existing, not found error", + fields: fields{ + eventstore: eventstoreExpect( + t, + expectFilter(), + ), + }, + args: args{ + ctx: context.Background(), + projectID: "project1", + appID: "app1", + resourceOwner: "org1", + }, + res: res{ + err: caos_errs.IsNotFound, + }, + }, + { + name: "change secret, ok", + fields: fields{ + eventstore: eventstoreExpect( + t, + expectFilter( + eventFromEventPusher( + project.NewApplicationAddedEvent(context.Background(), + &project.NewAggregate("project1", "org1").Aggregate, + "app1", + "app", + ), + ), + eventFromEventPusher( + project.NewAPIConfigAddedEvent(context.Background(), + &project.NewAggregate("project1", "org1").Aggregate, + "app1", + "client1@project", + &crypto.CryptoValue{ + CryptoType: crypto.TypeEncryption, + Algorithm: "enc", + KeyID: "id", + Crypted: []byte("a"), + }, + domain.APIAuthMethodTypeBasic), + ), + ), + expectPush( + []*repository.Event{ + eventFromEventPusher( + project.NewAPIConfigSecretChangedEvent(context.Background(), + &project.NewAggregate("project1", "org1").Aggregate, + "app1", + &crypto.CryptoValue{ + CryptoType: crypto.TypeEncryption, + Algorithm: "enc", + KeyID: "id", + Crypted: []byte("a"), + }), + ), + }, + ), + ), + secretGenerator: GetMockSecretGenerator(t), + }, + args: args{ + ctx: context.Background(), + projectID: "project1", + appID: "app1", + resourceOwner: "org1", + }, + res: res{ + want: &domain.APIApp{ + ObjectRoot: models.ObjectRoot{ + AggregateID: "project1", + ResourceOwner: "org1", + }, + AppID: "app1", + AppName: "app", + ClientID: "client1@project", + ClientSecretString: "a", + AuthMethodType: domain.APIAuthMethodTypeBasic, + State: domain.AppStateActive, + }, + }, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + r := &Commands{ + eventstore: tt.fields.eventstore, + applicationSecretGenerator: tt.fields.secretGenerator, + } + got, err := r.ChangeAPIApplicationSecret(tt.args.ctx, tt.args.projectID, tt.args.appID, tt.args.resourceOwner) + 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 newAPIAppChangedEvent(ctx context.Context, appID, projectID, resourceOwner string, authMethodType domain.APIAuthMethodType) *project.APIConfigChangedEvent { + changes := []project.APIConfigChanges{ + project.ChangeAPIAuthMethodType(authMethodType), + } + event, _ := project.NewAPIConfigChangedEvent(ctx, + &project.NewAggregate(projectID, resourceOwner).Aggregate, + appID, + changes, + ) + return event +} diff --git a/internal/command/project_application_key.go b/internal/command/project_application_key.go index 1edf9323c..911791eb8 100644 --- a/internal/command/project_application_key.go +++ b/internal/command/project_application_key.go @@ -9,6 +9,9 @@ import ( ) func (c *Commands) AddApplicationKey(ctx context.Context, key *domain.ApplicationKey, resourceOwner string) (_ *domain.ApplicationKey, err error) { + if key.AggregateID == "" || key.ApplicationID == "" { + return nil, errors.ThrowInvalidArgument(nil, "COMMAND-55m9fs", "Errors.IDMissing") + } application, err := c.getApplicationWriteModel(ctx, key.AggregateID, key.ApplicationID, resourceOwner) if err != nil { return nil, err diff --git a/internal/command/project_application_key_test.go b/internal/command/project_application_key_test.go new file mode 100644 index 000000000..cc4ead388 --- /dev/null +++ b/internal/command/project_application_key_test.go @@ -0,0 +1,215 @@ +package command + +import ( + "context" + "github.com/caos/zitadel/internal/crypto" + "github.com/caos/zitadel/internal/domain" + caos_errs "github.com/caos/zitadel/internal/errors" + "github.com/caos/zitadel/internal/eventstore" + "github.com/caos/zitadel/internal/eventstore/v1/models" + "github.com/caos/zitadel/internal/id" + id_mock "github.com/caos/zitadel/internal/id/mock" + "github.com/caos/zitadel/internal/repository/project" + "github.com/stretchr/testify/assert" + "testing" +) + +func TestCommandSide_AddAPIApplicationKey(t *testing.T) { + type fields struct { + eventstore *eventstore.Eventstore + idGenerator id.Generator + secretGenerator crypto.Generator + keySize int + } + type args struct { + ctx context.Context + key *domain.ApplicationKey + resourceOwner string + } + type res struct { + want *domain.APIApp + err func(error) bool + } + tests := []struct { + name string + fields fields + args args + res res + }{ + { + name: "no aggregateid, invalid argument error", + fields: fields{ + eventstore: eventstoreExpect( + t, + ), + }, + args: args{ + ctx: context.Background(), + key: &domain.ApplicationKey{ + ApplicationID: "app1", + }, + resourceOwner: "org1", + }, + res: res{ + err: caos_errs.IsErrorInvalidArgument, + }, + }, + { + name: "no appid, invalid argument error", + fields: fields{ + eventstore: eventstoreExpect( + t, + ), + }, + args: args{ + ctx: context.Background(), + key: &domain.ApplicationKey{ + ObjectRoot: models.ObjectRoot{ + AggregateID: "project1", + }, + }, + resourceOwner: "org1", + }, + res: res{ + err: caos_errs.IsErrorInvalidArgument, + }, + }, + { + name: "app not existing, not found error", + fields: fields{ + eventstore: eventstoreExpect( + t, + expectFilter(), + ), + }, + args: args{ + ctx: context.Background(), + key: &domain.ApplicationKey{ + ObjectRoot: models.ObjectRoot{ + AggregateID: "project1", + }, + ApplicationID: "app1", + }, + resourceOwner: "org1", + }, + res: res{ + err: caos_errs.IsPreconditionFailed, + }, + }, + { + name: "create key not allowed, precondition error", + fields: fields{ + eventstore: eventstoreExpect( + t, + expectFilter( + eventFromEventPusher( + project.NewApplicationAddedEvent(context.Background(), + &project.NewAggregate("project1", "org1").Aggregate, + "app1", + "app", + ), + ), + ), + expectFilter( + eventFromEventPusher( + project.NewAPIConfigAddedEvent(context.Background(), + &project.NewAggregate("project1", "org1").Aggregate, + "app1", + "client1@project", + &crypto.CryptoValue{ + CryptoType: crypto.TypeEncryption, + Algorithm: "enc", + KeyID: "id", + Crypted: []byte("a"), + }, + domain.APIAuthMethodTypeBasic), + ), + ), + ), + idGenerator: id_mock.NewIDGeneratorExpectIDs(t, "key1"), + secretGenerator: GetMockSecretGenerator(t), + }, + args: args{ + ctx: context.Background(), + key: &domain.ApplicationKey{ + ObjectRoot: models.ObjectRoot{ + AggregateID: "project1", + }, + ApplicationID: "app1", + }, + resourceOwner: "org1", + }, + res: res{ + err: caos_errs.IsPreconditionFailed, + }, + }, + { + name: "create key not allowed, precondition error", + fields: fields{ + eventstore: eventstoreExpect( + t, + expectFilter( + eventFromEventPusher( + project.NewApplicationAddedEvent(context.Background(), + &project.NewAggregate("project1", "org1").Aggregate, + "app1", + "app", + ), + ), + ), + expectFilter( + eventFromEventPusher( + project.NewAPIConfigAddedEvent(context.Background(), + &project.NewAggregate("project1", "org1").Aggregate, + "app1", + "client1@project", + &crypto.CryptoValue{ + CryptoType: crypto.TypeEncryption, + Algorithm: "enc", + KeyID: "id", + Crypted: []byte("a"), + }, + domain.APIAuthMethodTypeBasic), + ), + ), + ), + idGenerator: id_mock.NewIDGeneratorExpectIDs(t, "key1"), + secretGenerator: GetMockSecretGenerator(t), + keySize: 10, + }, + args: args{ + ctx: context.Background(), + key: &domain.ApplicationKey{ + ObjectRoot: models.ObjectRoot{ + AggregateID: "project1", + }, + ApplicationID: "app1", + }, + resourceOwner: "org1", + }, + res: res{ + err: caos_errs.IsPreconditionFailed, + }, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + r := &Commands{ + eventstore: tt.fields.eventstore, + idGenerator: tt.fields.idGenerator, + applicationSecretGenerator: tt.fields.secretGenerator, + applicationKeySize: tt.fields.keySize, + } + got, err := r.AddApplicationKey(tt.args.ctx, tt.args.key, tt.args.resourceOwner) + 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) + } + }) + } +} diff --git a/internal/command/project_application_oidc.go b/internal/command/project_application_oidc.go index e112b6c24..67814abc3 100644 --- a/internal/command/project_application_oidc.go +++ b/internal/command/project_application_oidc.go @@ -14,9 +14,12 @@ import ( ) func (c *Commands) AddOIDCApplication(ctx context.Context, application *domain.OIDCApp, resourceOwner string) (_ *domain.OIDCApp, err error) { + if application == nil || application.AggregateID == "" { + return nil, caos_errs.ThrowInvalidArgument(nil, "PROJECT-34Fm0", "Errors.Application.Invalid") + } project, err := c.getProjectByID(ctx, application.AggregateID, resourceOwner) if err != nil { - return nil, err + return nil, caos_errs.ThrowPreconditionFailed(err, "PROJECT-3m9ss", "Errors.Project.NotFound") } addedApplication := NewOIDCApplicationWriteModel(application.AggregateID, resourceOwner) projectAgg := ProjectAggregateFromWriteModel(&addedApplication.WriteModel) @@ -41,7 +44,7 @@ func (c *Commands) AddOIDCApplication(ctx context.Context, application *domain.O func (c *Commands) addOIDCApplication(ctx context.Context, projectAgg *eventstore.Aggregate, proj *domain.Project, oidcApp *domain.OIDCApp, resourceOwner string) (events []eventstore.EventPusher, stringPW string, err error) { if !oidcApp.IsValid() { - return nil, "", caos_errs.ThrowPreconditionFailed(nil, "PROJECT-Bff2g", "Errors.Application.Invalid") + return nil, "", caos_errs.ThrowInvalidArgument(nil, "PROJECT-Bff2g", "Errors.Application.Invalid") } oidcApp.AppID, err = c.idGenerator.Next() if err != nil { @@ -49,7 +52,7 @@ func (c *Commands) addOIDCApplication(ctx context.Context, projectAgg *eventstor } events = []eventstore.EventPusher{ - project.NewApplicationAddedEvent(ctx, projectAgg, oidcApp.AppID, oidcApp.AppName, resourceOwner), + project.NewApplicationAddedEvent(ctx, projectAgg, oidcApp.AppID, oidcApp.AppName), } var stringPw string @@ -84,8 +87,8 @@ func (c *Commands) addOIDCApplication(ctx context.Context, projectAgg *eventstor } func (c *Commands) ChangeOIDCApplication(ctx context.Context, oidc *domain.OIDCApp, resourceOwner string) (*domain.OIDCApp, error) { - if !oidc.IsValid() { - return nil, caos_errs.ThrowPreconditionFailed(nil, "COMMAND-1m900", "Errors.Project.App.OIDCConfigInvalid") + if !oidc.IsValid() || oidc.AppID == "" || oidc.AggregateID == "" { + return nil, caos_errs.ThrowInvalidArgument(nil, "COMMAND-5m9fs", "Errors.Project.App.OIDCConfigInvalid") } existingOIDC, err := c.getOIDCAppWriteModel(ctx, oidc.AggregateID, oidc.AppID, resourceOwner) @@ -136,7 +139,7 @@ func (c *Commands) ChangeOIDCApplication(ctx context.Context, oidc *domain.OIDCA func (c *Commands) ChangeOIDCApplicationSecret(ctx context.Context, projectID, appID, resourceOwner string) (*domain.OIDCApp, error) { if projectID == "" || appID == "" { - return nil, caos_errs.ThrowPreconditionFailed(nil, "COMMAND-99i83", "Errors.IDMissing") + return nil, caos_errs.ThrowInvalidArgument(nil, "COMMAND-99i83", "Errors.IDMissing") } existingOIDC, err := c.getOIDCAppWriteModel(ctx, projectID, appID, resourceOwner) diff --git a/internal/command/project_application_oidc_test.go b/internal/command/project_application_oidc_test.go new file mode 100644 index 000000000..1635a2830 --- /dev/null +++ b/internal/command/project_application_oidc_test.go @@ -0,0 +1,734 @@ +package command + +import ( + "context" + "github.com/caos/zitadel/internal/crypto" + "github.com/caos/zitadel/internal/domain" + caos_errs "github.com/caos/zitadel/internal/errors" + "github.com/caos/zitadel/internal/eventstore" + "github.com/caos/zitadel/internal/eventstore/repository" + "github.com/caos/zitadel/internal/eventstore/v1/models" + "github.com/caos/zitadel/internal/id" + id_mock "github.com/caos/zitadel/internal/id/mock" + "github.com/caos/zitadel/internal/repository/project" + "github.com/stretchr/testify/assert" + "testing" + "time" +) + +func TestCommandSide_AddOIDCApplication(t *testing.T) { + type fields struct { + eventstore *eventstore.Eventstore + idGenerator id.Generator + secretGenerator crypto.Generator + } + type args struct { + ctx context.Context + oidcApp *domain.OIDCApp + resourceOwner string + } + type res struct { + want *domain.OIDCApp + err func(error) bool + } + tests := []struct { + name string + fields fields + args args + res res + }{ + { + name: "no aggregate id, invalid argument error", + fields: fields{ + eventstore: eventstoreExpect( + t, + ), + }, + args: args{ + ctx: context.Background(), + oidcApp: &domain.OIDCApp{}, + resourceOwner: "org1", + }, + res: res{ + err: caos_errs.IsErrorInvalidArgument, + }, + }, + { + name: "project not existing, not found error", + fields: fields{ + eventstore: eventstoreExpect( + t, + expectFilter(), + ), + }, + args: args{ + ctx: context.Background(), + oidcApp: &domain.OIDCApp{ + ObjectRoot: models.ObjectRoot{ + AggregateID: "project1", + }, + AppID: "app1", + AppName: "app", + }, + resourceOwner: "org1", + }, + res: res{ + err: caos_errs.IsPreconditionFailed, + }, + }, + { + name: "invalid app, invalid argument error", + fields: fields{ + eventstore: eventstoreExpect( + t, + expectFilter( + eventFromEventPusher( + project.NewProjectAddedEvent(context.Background(), + &project.NewAggregate("project1", "org1").Aggregate, + "project"), + ), + ), + ), + }, + args: args{ + ctx: context.Background(), + oidcApp: &domain.OIDCApp{ + ObjectRoot: models.ObjectRoot{ + AggregateID: "project1", + }, + AppID: "app1", + AppName: "", + }, + resourceOwner: "org1", + }, + res: res{ + err: caos_errs.IsErrorInvalidArgument, + }, + }, + { + name: "create oidc app basic, ok", + fields: fields{ + eventstore: eventstoreExpect( + t, + expectFilter( + eventFromEventPusher( + project.NewProjectAddedEvent(context.Background(), + &project.NewAggregate("project1", "org1").Aggregate, + "project"), + ), + ), + expectPush( + []*repository.Event{ + eventFromEventPusher( + project.NewApplicationAddedEvent(context.Background(), + &project.NewAggregate("project1", "org1").Aggregate, + "app1", + "app", + ), + ), + eventFromEventPusher( + project.NewOIDCConfigAddedEvent(context.Background(), + &project.NewAggregate("project1", "org1").Aggregate, + domain.OIDCVersionV1, + "app1", + "client1@project", + &crypto.CryptoValue{ + CryptoType: crypto.TypeEncryption, + Algorithm: "enc", + KeyID: "id", + Crypted: []byte("a"), + }, + []string{"https://test.ch"}, + []domain.OIDCResponseType{domain.OIDCResponseTypeCode}, + []domain.OIDCGrantType{domain.OIDCGrantTypeAuthorizationCode}, + domain.OIDCApplicationTypeWeb, + domain.OIDCAuthMethodTypePost, + []string{"https://test.ch/logout"}, + true, + domain.OIDCTokenTypeBearer, + true, + true, + true, + time.Second*1), + ), + }, + uniqueConstraintsFromEventConstraint(project.NewAddApplicationUniqueConstraint("app", "project1")), + ), + ), + idGenerator: id_mock.NewIDGeneratorExpectIDs(t, "app1", "client1"), + secretGenerator: GetMockSecretGenerator(t), + }, + args: args{ + ctx: context.Background(), + oidcApp: &domain.OIDCApp{ + ObjectRoot: models.ObjectRoot{ + AggregateID: "project1", + }, + AppName: "app", + AuthMethodType: domain.OIDCAuthMethodTypePost, + OIDCVersion: domain.OIDCVersionV1, + RedirectUris: []string{"https://test.ch"}, + ResponseTypes: []domain.OIDCResponseType{domain.OIDCResponseTypeCode}, + GrantTypes: []domain.OIDCGrantType{domain.OIDCGrantTypeAuthorizationCode}, + ApplicationType: domain.OIDCApplicationTypeWeb, + PostLogoutRedirectUris: []string{"https://test.ch/logout"}, + DevMode: true, + AccessTokenType: domain.OIDCTokenTypeBearer, + AccessTokenRoleAssertion: true, + IDTokenRoleAssertion: true, + IDTokenUserinfoAssertion: true, + ClockSkew: time.Second * 1, + }, + resourceOwner: "org1", + }, + res: res{ + want: &domain.OIDCApp{ + ObjectRoot: models.ObjectRoot{ + AggregateID: "project1", + ResourceOwner: "org1", + }, + AppID: "app1", + AppName: "app", + ClientID: "client1@project", + ClientSecretString: "a", + AuthMethodType: domain.OIDCAuthMethodTypePost, + OIDCVersion: domain.OIDCVersionV1, + RedirectUris: []string{"https://test.ch"}, + ResponseTypes: []domain.OIDCResponseType{domain.OIDCResponseTypeCode}, + GrantTypes: []domain.OIDCGrantType{domain.OIDCGrantTypeAuthorizationCode}, + ApplicationType: domain.OIDCApplicationTypeWeb, + PostLogoutRedirectUris: []string{"https://test.ch/logout"}, + DevMode: true, + AccessTokenType: domain.OIDCTokenTypeBearer, + AccessTokenRoleAssertion: true, + IDTokenRoleAssertion: true, + IDTokenUserinfoAssertion: true, + ClockSkew: time.Second * 1, + State: domain.AppStateActive, + Compliance: &domain.Compliance{}, + }, + }, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + r := &Commands{ + eventstore: tt.fields.eventstore, + idGenerator: tt.fields.idGenerator, + applicationSecretGenerator: tt.fields.secretGenerator, + } + got, err := r.AddOIDCApplication(tt.args.ctx, tt.args.oidcApp, tt.args.resourceOwner) + 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_ChangeOIDCApplication(t *testing.T) { + type fields struct { + eventstore *eventstore.Eventstore + } + type args struct { + ctx context.Context + oidcApp *domain.OIDCApp + resourceOwner string + } + type res struct { + want *domain.OIDCApp + err func(error) bool + } + tests := []struct { + name string + fields fields + args args + res res + }{ + { + name: "invalid app, invalid argument error", + fields: fields{ + eventstore: eventstoreExpect( + t, + ), + }, + args: args{ + ctx: context.Background(), + oidcApp: &domain.OIDCApp{ + ObjectRoot: models.ObjectRoot{ + AggregateID: "project1", + }, + AppID: "app1", + }, + resourceOwner: "org1", + }, + res: res{ + err: caos_errs.IsErrorInvalidArgument, + }, + }, + { + name: "missing appid, invalid argument error", + fields: fields{ + eventstore: eventstoreExpect( + t, + ), + }, + args: args{ + ctx: context.Background(), + oidcApp: &domain.OIDCApp{ + ObjectRoot: models.ObjectRoot{ + AggregateID: "project1", + }, + AppID: "", + AppName: "app", + AuthMethodType: domain.OIDCAuthMethodTypePost, + }, + resourceOwner: "org1", + }, + res: res{ + err: caos_errs.IsErrorInvalidArgument, + }, + }, + { + name: "missing aggregateid, invalid argument error", + fields: fields{ + eventstore: eventstoreExpect( + t, + ), + }, + args: args{ + ctx: context.Background(), + oidcApp: &domain.OIDCApp{ + ObjectRoot: models.ObjectRoot{ + AggregateID: "", + }, + AppID: "appid", + AppName: "app", + AuthMethodType: domain.OIDCAuthMethodTypePost, + }, + resourceOwner: "org1", + }, + res: res{ + err: caos_errs.IsErrorInvalidArgument, + }, + }, + { + name: "app not existing, not found error", + fields: fields{ + eventstore: eventstoreExpect( + t, + expectFilter(), + ), + }, + args: args{ + ctx: context.Background(), + oidcApp: &domain.OIDCApp{ + ObjectRoot: models.ObjectRoot{ + AggregateID: "project1", + }, + AppID: "app1", + AppName: "app", + }, + resourceOwner: "org1", + }, + res: res{ + err: caos_errs.IsNotFound, + }, + }, + { + name: "no changes, precondition error", + fields: fields{ + eventstore: eventstoreExpect( + t, + expectFilter( + eventFromEventPusher( + project.NewApplicationAddedEvent(context.Background(), + &project.NewAggregate("project1", "org1").Aggregate, + "app1", + "app", + ), + ), + eventFromEventPusher( + project.NewOIDCConfigAddedEvent(context.Background(), + &project.NewAggregate("project1", "org1").Aggregate, + domain.OIDCVersionV1, + "app1", + "client1@project", + &crypto.CryptoValue{ + CryptoType: crypto.TypeEncryption, + Algorithm: "enc", + KeyID: "id", + Crypted: []byte("a"), + }, + []string{"https://test.ch"}, + []domain.OIDCResponseType{domain.OIDCResponseTypeCode}, + []domain.OIDCGrantType{domain.OIDCGrantTypeAuthorizationCode}, + domain.OIDCApplicationTypeWeb, + domain.OIDCAuthMethodTypePost, + []string{"https://test.ch/logout"}, + true, + domain.OIDCTokenTypeBearer, + true, + true, + true, + time.Second*1), + ), + ), + ), + }, + args: args{ + ctx: context.Background(), + oidcApp: &domain.OIDCApp{ + ObjectRoot: models.ObjectRoot{ + AggregateID: "project1", + }, + AppID: "app1", + AppName: "app", + AuthMethodType: domain.OIDCAuthMethodTypePost, + OIDCVersion: domain.OIDCVersionV1, + RedirectUris: []string{"https://test.ch"}, + ResponseTypes: []domain.OIDCResponseType{domain.OIDCResponseTypeCode}, + GrantTypes: []domain.OIDCGrantType{domain.OIDCGrantTypeAuthorizationCode}, + ApplicationType: domain.OIDCApplicationTypeWeb, + PostLogoutRedirectUris: []string{"https://test.ch/logout"}, + DevMode: true, + AccessTokenType: domain.OIDCTokenTypeBearer, + AccessTokenRoleAssertion: true, + IDTokenRoleAssertion: true, + IDTokenUserinfoAssertion: true, + ClockSkew: time.Second * 1, + }, + resourceOwner: "org1", + }, + res: res{ + err: caos_errs.IsPreconditionFailed, + }, + }, + { + name: "change oidc app, ok", + fields: fields{ + eventstore: eventstoreExpect( + t, + expectFilter( + eventFromEventPusher( + project.NewApplicationAddedEvent(context.Background(), + &project.NewAggregate("project1", "org1").Aggregate, + "app1", + "app", + ), + ), + eventFromEventPusher( + project.NewOIDCConfigAddedEvent(context.Background(), + &project.NewAggregate("project1", "org1").Aggregate, + domain.OIDCVersionV1, + "app1", + "client1@project", + &crypto.CryptoValue{ + CryptoType: crypto.TypeEncryption, + Algorithm: "enc", + KeyID: "id", + Crypted: []byte("a"), + }, + []string{"https://test.ch"}, + []domain.OIDCResponseType{domain.OIDCResponseTypeCode}, + []domain.OIDCGrantType{domain.OIDCGrantTypeAuthorizationCode}, + domain.OIDCApplicationTypeWeb, + domain.OIDCAuthMethodTypePost, + []string{"https://test.ch/logout"}, + false, + domain.OIDCTokenTypeBearer, + true, + true, + true, + time.Second*1), + ), + ), + expectPush( + []*repository.Event{ + eventFromEventPusher( + newOIDCAppChangedEvent(context.Background(), + "app1", + "project1", + "org1"), + ), + }, + ), + ), + }, + args: args{ + ctx: context.Background(), + oidcApp: &domain.OIDCApp{ + ObjectRoot: models.ObjectRoot{ + AggregateID: "project1", + }, + AppID: "app1", + AppName: "app", + AuthMethodType: domain.OIDCAuthMethodTypePost, + OIDCVersion: domain.OIDCVersionV1, + RedirectUris: []string{"https://test-change.ch"}, + ResponseTypes: []domain.OIDCResponseType{domain.OIDCResponseTypeCode}, + GrantTypes: []domain.OIDCGrantType{domain.OIDCGrantTypeAuthorizationCode}, + ApplicationType: domain.OIDCApplicationTypeWeb, + PostLogoutRedirectUris: []string{"https://test-change.ch/logout"}, + DevMode: true, + AccessTokenType: domain.OIDCTokenTypeJWT, + AccessTokenRoleAssertion: false, + IDTokenRoleAssertion: false, + IDTokenUserinfoAssertion: false, + ClockSkew: time.Second * 2, + }, + resourceOwner: "org1", + }, + res: res{ + want: &domain.OIDCApp{ + ObjectRoot: models.ObjectRoot{ + AggregateID: "project1", + ResourceOwner: "org1", + }, + AppID: "app1", + ClientID: "client1@project", + AppName: "app", + AuthMethodType: domain.OIDCAuthMethodTypePost, + OIDCVersion: domain.OIDCVersionV1, + RedirectUris: []string{"https://test-change.ch"}, + ResponseTypes: []domain.OIDCResponseType{domain.OIDCResponseTypeCode}, + GrantTypes: []domain.OIDCGrantType{domain.OIDCGrantTypeAuthorizationCode}, + ApplicationType: domain.OIDCApplicationTypeWeb, + PostLogoutRedirectUris: []string{"https://test-change.ch/logout"}, + DevMode: true, + AccessTokenType: domain.OIDCTokenTypeJWT, + AccessTokenRoleAssertion: false, + IDTokenRoleAssertion: false, + IDTokenUserinfoAssertion: false, + ClockSkew: time.Second * 2, + Compliance: &domain.Compliance{}, + State: domain.AppStateActive, + }, + }, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + r := &Commands{ + eventstore: tt.fields.eventstore, + } + got, err := r.ChangeOIDCApplication(tt.args.ctx, tt.args.oidcApp, tt.args.resourceOwner) + 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_ChangeOIDCApplicationSecret(t *testing.T) { + type fields struct { + eventstore *eventstore.Eventstore + secretGenerator crypto.Generator + } + type args struct { + ctx context.Context + appID string + projectID string + resourceOwner string + } + type res struct { + want *domain.OIDCApp + err func(error) bool + } + tests := []struct { + name string + fields fields + args args + res res + }{ + { + name: "no projectid, invalid argument error", + fields: fields{ + eventstore: eventstoreExpect( + t, + ), + }, + args: args{ + ctx: context.Background(), + appID: "app1", + resourceOwner: "org1", + }, + res: res{ + err: caos_errs.IsErrorInvalidArgument, + }, + }, + { + name: "no appid, invalid argument error", + fields: fields{ + eventstore: eventstoreExpect( + t, + ), + }, + args: args{ + ctx: context.Background(), + projectID: "project1", + appID: "", + resourceOwner: "org1", + }, + res: res{ + err: caos_errs.IsErrorInvalidArgument, + }, + }, + { + name: "app not existing, not found error", + fields: fields{ + eventstore: eventstoreExpect( + t, + expectFilter(), + ), + }, + args: args{ + ctx: context.Background(), + projectID: "project1", + appID: "app1", + resourceOwner: "org1", + }, + res: res{ + err: caos_errs.IsNotFound, + }, + }, + { + name: "change secret, ok", + fields: fields{ + eventstore: eventstoreExpect( + t, + expectFilter( + eventFromEventPusher( + project.NewApplicationAddedEvent(context.Background(), + &project.NewAggregate("project1", "org1").Aggregate, + "app1", + "app", + ), + ), + eventFromEventPusher( + project.NewOIDCConfigAddedEvent(context.Background(), + &project.NewAggregate("project1", "org1").Aggregate, + domain.OIDCVersionV1, + "app1", + "client1@project", + &crypto.CryptoValue{ + CryptoType: crypto.TypeEncryption, + Algorithm: "enc", + KeyID: "id", + Crypted: []byte("a"), + }, + []string{"https://test.ch"}, + []domain.OIDCResponseType{domain.OIDCResponseTypeCode}, + []domain.OIDCGrantType{domain.OIDCGrantTypeAuthorizationCode}, + domain.OIDCApplicationTypeWeb, + domain.OIDCAuthMethodTypePost, + []string{"https://test.ch/logout"}, + true, + domain.OIDCTokenTypeBearer, + true, + true, + true, + time.Second*1), + ), + ), + expectPush( + []*repository.Event{ + eventFromEventPusher( + project.NewOIDCConfigSecretChangedEvent(context.Background(), + &project.NewAggregate("project1", "org1").Aggregate, + "app1", + &crypto.CryptoValue{ + CryptoType: crypto.TypeEncryption, + Algorithm: "enc", + KeyID: "id", + Crypted: []byte("a"), + }), + ), + }, + ), + ), + secretGenerator: GetMockSecretGenerator(t), + }, + args: args{ + ctx: context.Background(), + projectID: "project1", + appID: "app1", + resourceOwner: "org1", + }, + res: res{ + want: &domain.OIDCApp{ + ObjectRoot: models.ObjectRoot{ + AggregateID: "project1", + ResourceOwner: "org1", + }, + AppID: "app1", + AppName: "app", + ClientID: "client1@project", + ClientSecretString: "a", + AuthMethodType: domain.OIDCAuthMethodTypePost, + OIDCVersion: domain.OIDCVersionV1, + RedirectUris: []string{"https://test.ch"}, + ResponseTypes: []domain.OIDCResponseType{domain.OIDCResponseTypeCode}, + GrantTypes: []domain.OIDCGrantType{domain.OIDCGrantTypeAuthorizationCode}, + ApplicationType: domain.OIDCApplicationTypeWeb, + PostLogoutRedirectUris: []string{"https://test.ch/logout"}, + DevMode: true, + AccessTokenType: domain.OIDCTokenTypeBearer, + AccessTokenRoleAssertion: true, + IDTokenRoleAssertion: true, + IDTokenUserinfoAssertion: true, + ClockSkew: time.Second * 1, + State: domain.AppStateActive, + }, + }, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + r := &Commands{ + eventstore: tt.fields.eventstore, + applicationSecretGenerator: tt.fields.secretGenerator, + } + got, err := r.ChangeOIDCApplicationSecret(tt.args.ctx, tt.args.projectID, tt.args.appID, tt.args.resourceOwner) + 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 newOIDCAppChangedEvent(ctx context.Context, appID, projectID, resourceOwner string) *project.OIDCConfigChangedEvent { + changes := []project.OIDCConfigChanges{ + project.ChangeRedirectURIs([]string{"https://test-change.ch"}), + project.ChangePostLogoutRedirectURIs([]string{"https://test-change.ch/logout"}), + project.ChangeDevMode(true), + project.ChangeAccessTokenType(domain.OIDCTokenTypeJWT), + project.ChangeAccessTokenRoleAssertion(false), + project.ChangeIDTokenRoleAssertion(false), + project.ChangeIDTokenUserinfoAssertion(false), + project.ChangeClockSkew(time.Second * 2), + } + event, _ := project.NewOIDCConfigChangedEvent(ctx, + &project.NewAggregate(projectID, resourceOwner).Aggregate, + appID, + changes, + ) + return event +} diff --git a/internal/command/project_application_test.go b/internal/command/project_application_test.go new file mode 100644 index 000000000..88ef37eb4 --- /dev/null +++ b/internal/command/project_application_test.go @@ -0,0 +1,636 @@ +package command + +import ( + "context" + "github.com/caos/zitadel/internal/domain" + caos_errs "github.com/caos/zitadel/internal/errors" + "github.com/caos/zitadel/internal/eventstore" + "github.com/caos/zitadel/internal/eventstore/repository" + "github.com/caos/zitadel/internal/repository/project" + "github.com/stretchr/testify/assert" + "testing" +) + +func TestCommandSide_ChangeApplication(t *testing.T) { + type fields struct { + eventstore *eventstore.Eventstore + } + type args struct { + ctx context.Context + projectID string + app *domain.ChangeApp + resourceOwner string + } + type res struct { + want *domain.ObjectDetails + err func(error) bool + } + tests := []struct { + name string + fields fields + args args + res res + }{ + { + name: "invalid app missing projectid, invalid argument error", + fields: fields{ + eventstore: eventstoreExpect( + t, + ), + }, + args: args{ + ctx: context.Background(), + projectID: "", + app: &domain.ChangeApp{ + AppID: "app1", + AppName: "app", + }, + resourceOwner: "org1", + }, + res: res{ + err: caos_errs.IsErrorInvalidArgument, + }, + }, + { + name: "invalid app missing appid, invalid argument error", + fields: fields{ + eventstore: eventstoreExpect( + t, + ), + }, + args: args{ + ctx: context.Background(), + projectID: "project1", + app: &domain.ChangeApp{ + AppName: "app", + }, + resourceOwner: "org1", + }, + res: res{ + err: caos_errs.IsErrorInvalidArgument, + }, + }, + { + name: "invalid app missing name, invalid argument error", + fields: fields{ + eventstore: eventstoreExpect( + t, + ), + }, + args: args{ + ctx: context.Background(), + projectID: "project1", + app: &domain.ChangeApp{ + AppID: "app1", + AppName: "", + }, + resourceOwner: "org1", + }, + res: res{ + err: caos_errs.IsErrorInvalidArgument, + }, + }, + { + name: "app not existing, not found error", + fields: fields{ + eventstore: eventstoreExpect( + t, + expectFilter(), + ), + }, + args: args{ + ctx: context.Background(), + projectID: "project1", + app: &domain.ChangeApp{ + AppID: "app1", + AppName: "app", + }, + resourceOwner: "org1", + }, + res: res{ + err: caos_errs.IsNotFound, + }, + }, + { + name: "app name not changed, not found error", + fields: fields{ + eventstore: eventstoreExpect( + t, + expectFilter( + eventFromEventPusher(project.NewApplicationAddedEvent(context.Background(), + &project.NewAggregate("project1", "org1").Aggregate, + "app1", + "app", + )), + ), + ), + }, + args: args{ + ctx: context.Background(), + projectID: "project1", + app: &domain.ChangeApp{ + AppID: "app1", + AppName: "app", + }, + resourceOwner: "org1", + }, + res: res{ + err: caos_errs.IsPreconditionFailed, + }, + }, + { + name: "app changed, ok", + fields: fields{ + eventstore: eventstoreExpect( + t, + expectFilter( + eventFromEventPusher(project.NewApplicationAddedEvent(context.Background(), + &project.NewAggregate("project1", "org1").Aggregate, + "app1", + "app", + )), + ), + expectPush( + []*repository.Event{ + eventFromEventPusher(project.NewApplicationChangedEvent(context.Background(), + &project.NewAggregate("project1", "org1").Aggregate, + "app1", + "app", + "app changed", + )), + }, + uniqueConstraintsFromEventConstraint(project.NewRemoveApplicationUniqueConstraint("app", "project1")), + uniqueConstraintsFromEventConstraint(project.NewAddApplicationUniqueConstraint("app changed", "project1")), + ), + ), + }, + args: args{ + ctx: context.Background(), + projectID: "project1", + app: &domain.ChangeApp{ + AppID: "app1", + AppName: "app changed", + }, + resourceOwner: "org1", + }, + res: res{ + want: &domain.ObjectDetails{ + ResourceOwner: "org1", + }, + }, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + r := &Commands{ + eventstore: tt.fields.eventstore, + } + got, err := r.ChangeApplication(tt.args.ctx, tt.args.projectID, tt.args.app, tt.args.resourceOwner) + 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_DeactivateApplication(t *testing.T) { + type fields struct { + eventstore *eventstore.Eventstore + } + type args struct { + ctx context.Context + projectID string + appID string + resourceOwner string + } + type res struct { + want *domain.ObjectDetails + err func(error) bool + } + tests := []struct { + name string + fields fields + args args + res res + }{ + { + name: "missing projectid, invalid argument error", + fields: fields{ + eventstore: eventstoreExpect( + t, + ), + }, + args: args{ + ctx: context.Background(), + projectID: "", + appID: "app1", + resourceOwner: "org1", + }, + res: res{ + err: caos_errs.IsErrorInvalidArgument, + }, + }, + { + name: "missing appid, invalid argument error", + fields: fields{ + eventstore: eventstoreExpect( + t, + ), + }, + args: args{ + ctx: context.Background(), + projectID: "project1", + appID: "", + resourceOwner: "org1", + }, + res: res{ + err: caos_errs.IsErrorInvalidArgument, + }, + }, + { + name: "app not existing, not found error", + fields: fields{ + eventstore: eventstoreExpect( + t, + expectFilter(), + ), + }, + args: args{ + ctx: context.Background(), + projectID: "project1", + appID: "app1", + resourceOwner: "org1", + }, + res: res{ + err: caos_errs.IsNotFound, + }, + }, + { + name: "app already inactive, precondition error", + fields: fields{ + eventstore: eventstoreExpect( + t, + expectFilter( + eventFromEventPusher(project.NewApplicationAddedEvent(context.Background(), + &project.NewAggregate("project1", "org1").Aggregate, + "app1", + "app", + )), + eventFromEventPusher(project.NewApplicationDeactivatedEvent(context.Background(), + &project.NewAggregate("project1", "org1").Aggregate, + "app1", + )), + ), + ), + }, + args: args{ + ctx: context.Background(), + projectID: "project1", + appID: "app1", + resourceOwner: "org1", + }, + res: res{ + err: caos_errs.IsPreconditionFailed, + }, + }, + { + name: "app deactivate, ok", + fields: fields{ + eventstore: eventstoreExpect( + t, + expectFilter( + eventFromEventPusher(project.NewApplicationAddedEvent(context.Background(), + &project.NewAggregate("project1", "org1").Aggregate, + "app1", + "app", + )), + ), + expectPush( + []*repository.Event{ + eventFromEventPusher(project.NewApplicationDeactivatedEvent(context.Background(), + &project.NewAggregate("project1", "org1").Aggregate, + "app1", + )), + }, + ), + ), + }, + args: args{ + ctx: context.Background(), + projectID: "project1", + appID: "app1", + resourceOwner: "org1", + }, + res: res{ + want: &domain.ObjectDetails{ + ResourceOwner: "org1", + }, + }, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + r := &Commands{ + eventstore: tt.fields.eventstore, + } + got, err := r.DeactivateApplication(tt.args.ctx, tt.args.projectID, tt.args.appID, tt.args.resourceOwner) + 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_ReactivateApplication(t *testing.T) { + type fields struct { + eventstore *eventstore.Eventstore + } + type args struct { + ctx context.Context + projectID string + appID string + resourceOwner string + } + type res struct { + want *domain.ObjectDetails + err func(error) bool + } + tests := []struct { + name string + fields fields + args args + res res + }{ + { + name: "missing projectid, invalid argument error", + fields: fields{ + eventstore: eventstoreExpect( + t, + ), + }, + args: args{ + ctx: context.Background(), + projectID: "", + appID: "app1", + resourceOwner: "org1", + }, + res: res{ + err: caos_errs.IsErrorInvalidArgument, + }, + }, + { + name: "missing appid, invalid argument error", + fields: fields{ + eventstore: eventstoreExpect( + t, + ), + }, + args: args{ + ctx: context.Background(), + projectID: "project1", + appID: "", + resourceOwner: "org1", + }, + res: res{ + err: caos_errs.IsErrorInvalidArgument, + }, + }, + { + name: "app not existing, not found error", + fields: fields{ + eventstore: eventstoreExpect( + t, + expectFilter(), + ), + }, + args: args{ + ctx: context.Background(), + projectID: "project1", + appID: "app1", + resourceOwner: "org1", + }, + res: res{ + err: caos_errs.IsNotFound, + }, + }, + { + name: "app already active, precondition error", + fields: fields{ + eventstore: eventstoreExpect( + t, + expectFilter( + eventFromEventPusher(project.NewApplicationAddedEvent(context.Background(), + &project.NewAggregate("project1", "org1").Aggregate, + "app1", + "app", + )), + ), + ), + }, + args: args{ + ctx: context.Background(), + projectID: "project1", + appID: "app1", + resourceOwner: "org1", + }, + res: res{ + err: caos_errs.IsPreconditionFailed, + }, + }, + { + name: "app reactivate, ok", + fields: fields{ + eventstore: eventstoreExpect( + t, + expectFilter( + eventFromEventPusher(project.NewApplicationAddedEvent(context.Background(), + &project.NewAggregate("project1", "org1").Aggregate, + "app1", + "app", + )), + eventFromEventPusher(project.NewApplicationDeactivatedEvent(context.Background(), + &project.NewAggregate("project1", "org1").Aggregate, + "app1", + )), + ), + expectPush( + []*repository.Event{ + eventFromEventPusher(project.NewApplicationReactivatedEvent(context.Background(), + &project.NewAggregate("project1", "org1").Aggregate, + "app1", + )), + }, + ), + ), + }, + args: args{ + ctx: context.Background(), + projectID: "project1", + appID: "app1", + resourceOwner: "org1", + }, + res: res{ + want: &domain.ObjectDetails{ + ResourceOwner: "org1", + }, + }, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + r := &Commands{ + eventstore: tt.fields.eventstore, + } + got, err := r.ReactivateApplication(tt.args.ctx, tt.args.projectID, tt.args.appID, tt.args.resourceOwner) + 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_RemoveApplication(t *testing.T) { + type fields struct { + eventstore *eventstore.Eventstore + } + type args struct { + ctx context.Context + projectID string + appID string + resourceOwner string + } + type res struct { + want *domain.ObjectDetails + err func(error) bool + } + tests := []struct { + name string + fields fields + args args + res res + }{ + { + name: "missing projectid, invalid argument error", + fields: fields{ + eventstore: eventstoreExpect( + t, + ), + }, + args: args{ + ctx: context.Background(), + projectID: "", + appID: "app1", + resourceOwner: "org1", + }, + res: res{ + err: caos_errs.IsErrorInvalidArgument, + }, + }, + { + name: "missing appid, invalid argument error", + fields: fields{ + eventstore: eventstoreExpect( + t, + ), + }, + args: args{ + ctx: context.Background(), + projectID: "project1", + appID: "", + resourceOwner: "org1", + }, + res: res{ + err: caos_errs.IsErrorInvalidArgument, + }, + }, + { + name: "app not existing, not found error", + fields: fields{ + eventstore: eventstoreExpect( + t, + expectFilter(), + ), + }, + args: args{ + ctx: context.Background(), + projectID: "project1", + appID: "app1", + resourceOwner: "org1", + }, + res: res{ + err: caos_errs.IsNotFound, + }, + }, + { + name: "app remove, ok", + fields: fields{ + eventstore: eventstoreExpect( + t, + expectFilter( + eventFromEventPusher(project.NewApplicationAddedEvent(context.Background(), + &project.NewAggregate("project1", "org1").Aggregate, + "app1", + "app", + )), + ), + expectPush( + []*repository.Event{ + eventFromEventPusher(project.NewApplicationRemovedEvent(context.Background(), + &project.NewAggregate("project1", "org1").Aggregate, + "app1", + "app", + )), + }, + uniqueConstraintsFromEventConstraint(project.NewRemoveApplicationUniqueConstraint("app", "project1")), + ), + ), + }, + args: args{ + ctx: context.Background(), + projectID: "project1", + appID: "app1", + resourceOwner: "org1", + }, + res: res{ + want: &domain.ObjectDetails{ + ResourceOwner: "org1", + }, + }, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + r := &Commands{ + eventstore: tt.fields.eventstore, + } + got, err := r.RemoveApplication(tt.args.ctx, tt.args.projectID, tt.args.appID, tt.args.resourceOwner) + 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) + } + }) + } +} diff --git a/internal/command/project_converter.go b/internal/command/project_converter.go index 0409b8f86..220911c50 100644 --- a/internal/command/project_converter.go +++ b/internal/command/project_converter.go @@ -38,7 +38,6 @@ func oidcWriteModelToOIDCConfig(writeModel *OIDCApplicationWriteModel) *domain.O AppName: writeModel.AppName, State: writeModel.State, ClientID: writeModel.ClientID, - ClientSecret: writeModel.ClientSecret, RedirectUris: writeModel.RedirectUris, ResponseTypes: writeModel.ResponseTypes, GrantTypes: writeModel.GrantTypes, @@ -62,7 +61,6 @@ func apiWriteModelToAPIConfig(writeModel *APIApplicationWriteModel) *domain.APIA AppName: writeModel.AppName, State: writeModel.State, ClientID: writeModel.ClientID, - ClientSecret: writeModel.ClientSecret, AuthMethodType: writeModel.AuthMethodType, } } diff --git a/internal/command/project_grant.go b/internal/command/project_grant.go index 30cd372cc..9732e8669 100644 --- a/internal/command/project_grant.go +++ b/internal/command/project_grant.go @@ -13,25 +13,21 @@ import ( func (c *Commands) AddProjectGrant(ctx context.Context, grant *domain.ProjectGrant, resourceOwner string) (_ *domain.ProjectGrant, err error) { if !grant.IsValid() { - return nil, caos_errs.ThrowPreconditionFailed(nil, "PROJECT-Bff2g", "Errors.Project.Grant.Invalid") + return nil, caos_errs.ThrowInvalidArgument(nil, "PROJECT-Bff2g", "Errors.Project.Grant.Invalid") + } + err = c.checkProjectGrantPreCondition(ctx, grant) + if err != nil { + return nil, err } grant.GrantID, err = c.idGenerator.Next() if err != nil { return nil, err } - err = c.checkProjectExists(ctx, grant.AggregateID, resourceOwner) - if err != nil { - return nil, err - } - err = c.checkOrgExists(ctx, grant.GrantedOrgID) - if err != nil { - return nil, err - } addedGrant := NewProjectGrantWriteModel(grant.GrantID, grant.AggregateID, resourceOwner) projectAgg := ProjectAggregateFromWriteModel(&addedGrant.WriteModel) pushedEvents, err := c.eventstore.PushEvents( ctx, - project.NewGrantAddedEvent(ctx, projectAgg, grant.GrantID, grant.GrantedOrgID, grant.AggregateID, grant.RoleKeys)) + project.NewGrantAddedEvent(ctx, projectAgg, grant.GrantID, grant.GrantedOrgID, grant.RoleKeys)) if err != nil { return nil, err } @@ -44,13 +40,14 @@ func (c *Commands) AddProjectGrant(ctx context.Context, grant *domain.ProjectGra func (c *Commands) ChangeProjectGrant(ctx context.Context, grant *domain.ProjectGrant, resourceOwner string, cascadeUserGrantIDs ...string) (_ *domain.ProjectGrant, err error) { if grant.GrantID == "" { - return nil, caos_errs.ThrowPreconditionFailed(nil, "PROJECT-1j83s", "Errors.IDMissing") + return nil, caos_errs.ThrowInvalidArgument(nil, "PROJECT-1j83s", "Errors.IDMissing") } - err = c.checkProjectExists(ctx, grant.AggregateID, resourceOwner) + existingGrant, err := c.projectGrantWriteModelByID(ctx, grant.GrantID, grant.AggregateID, resourceOwner) if err != nil { return nil, err } - existingGrant, err := c.projectGrantWriteModelByID(ctx, grant.GrantID, grant.AggregateID, resourceOwner) + grant.GrantedOrgID = existingGrant.GrantedOrgID + err = c.checkProjectGrantPreCondition(ctx, grant) if err != nil { return nil, err } @@ -96,7 +93,7 @@ func (c *Commands) ChangeProjectGrant(ctx context.Context, grant *domain.Project } func (c *Commands) removeRoleFromProjectGrant(ctx context.Context, projectAgg *eventstore.Aggregate, projectID, projectGrantID, roleKey string, cascade bool) (_ eventstore.EventPusher, _ *ProjectGrantWriteModel, err error) { - existingProjectGrant, err := c.projectGrantWriteModelByID(ctx, projectID, projectGrantID, "") + existingProjectGrant, err := c.projectGrantWriteModelByID(ctx, projectGrantID, projectID, "") if err != nil { return nil, nil, err } @@ -127,7 +124,7 @@ func (c *Commands) removeRoleFromProjectGrant(ctx context.Context, projectAgg *e func (c *Commands) DeactivateProjectGrant(ctx context.Context, projectID, grantID, resourceOwner string) (details *domain.ObjectDetails, err error) { if grantID == "" || projectID == "" { - return details, caos_errs.ThrowPreconditionFailed(nil, "PROJECT-p0s4V", "Errors.IDMissing") + return details, caos_errs.ThrowInvalidArgument(nil, "PROJECT-p0s4V", "Errors.IDMissing") } err = c.checkProjectExists(ctx, projectID, resourceOwner) if err != nil { @@ -155,7 +152,7 @@ func (c *Commands) DeactivateProjectGrant(ctx context.Context, projectID, grantI func (c *Commands) ReactivateProjectGrant(ctx context.Context, projectID, grantID, resourceOwner string) (details *domain.ObjectDetails, err error) { if grantID == "" || projectID == "" { - return details, caos_errs.ThrowPreconditionFailed(nil, "PROJECT-p0s4V", "Errors.IDMissing") + return details, caos_errs.ThrowInvalidArgument(nil, "PROJECT-p0s4V", "Errors.IDMissing") } err = c.checkProjectExists(ctx, projectID, resourceOwner) if err != nil { @@ -182,11 +179,11 @@ func (c *Commands) ReactivateProjectGrant(ctx context.Context, projectID, grantI func (c *Commands) RemoveProjectGrant(ctx context.Context, projectID, grantID, resourceOwner string, cascadeUserGrantIDs ...string) (details *domain.ObjectDetails, err error) { if grantID == "" || projectID == "" { - return details, caos_errs.ThrowPreconditionFailed(nil, "PROJECT-1m9fJ", "Errors.IDMissing") + return details, caos_errs.ThrowInvalidArgument(nil, "PROJECT-1m9fJ", "Errors.IDMissing") } err = c.checkProjectExists(ctx, projectID, resourceOwner) if err != nil { - return details, err + return details, caos_errs.ThrowPreconditionFailed(err, "PROJECT-6mf9s", "Errors.Project.NotFound") } existingGrant, err := c.projectGrantWriteModelByID(ctx, grantID, projectID, resourceOwner) if err != nil { @@ -194,7 +191,7 @@ func (c *Commands) RemoveProjectGrant(ctx context.Context, projectID, grantID, r } events := make([]eventstore.EventPusher, 0) projectAgg := ProjectAggregateFromWriteModel(&existingGrant.WriteModel) - events = append(events, project.NewGrantRemovedEvent(ctx, projectAgg, grantID, existingGrant.GrantedOrgID, projectID)) + events = append(events, project.NewGrantRemovedEvent(ctx, projectAgg, grantID, existingGrant.GrantedOrgID)) for _, userGrantID := range cascadeUserGrantIDs { event, _, err := c.removeUserGrant(ctx, userGrantID, "", true) @@ -231,3 +228,21 @@ func (c *Commands) projectGrantWriteModelByID(ctx context.Context, grantID, proj return writeModel, nil } + +func (c *Commands) checkProjectGrantPreCondition(ctx context.Context, projectGrant *domain.ProjectGrant) error { + preConditions := NewProjectGrantPreConditionReadModel(projectGrant.AggregateID, projectGrant.GrantedOrgID) + err := c.eventstore.FilterToQueryReducer(ctx, preConditions) + if err != nil { + return err + } + if !preConditions.ProjectExists { + return caos_errs.ThrowPreconditionFailed(err, "COMMAND-m9gsd", "Errors.Project.NotFound") + } + if !preConditions.GrantedOrgExists { + return caos_errs.ThrowPreconditionFailed(err, "COMMAND-3m9gg", "Errors.Org.NotFound") + } + if projectGrant.HasInvalidRoles(preConditions.ExistingRoleKeys) { + return caos_errs.ThrowPreconditionFailed(err, "COMMAND-6m9gd", "Errors.Project.Role.NotFound") + } + return nil +} diff --git a/internal/command/project_grant_member.go b/internal/command/project_grant_member.go index 81e1918e4..013bfd04a 100644 --- a/internal/command/project_grant_member.go +++ b/internal/command/project_grant_member.go @@ -11,9 +11,12 @@ import ( "github.com/caos/zitadel/internal/telemetry/tracing" ) -func (c *Commands) AddProjectGrantMember(ctx context.Context, member *domain.ProjectGrantMember, resourceOwner string) (*domain.ProjectGrantMember, error) { +func (c *Commands) AddProjectGrantMember(ctx context.Context, member *domain.ProjectGrantMember) (*domain.ProjectGrantMember, error) { if !member.IsValid() { - return nil, caos_errs.ThrowPreconditionFailed(nil, "PROJECT-8fi7G", "Errors.Project.Member.Invalid") + return nil, caos_errs.ThrowInvalidArgument(nil, "PROJECT-8fi7G", "Errors.Project.Grant.Member.Invalid") + } + if len(domain.CheckForInvalidRoles(member.Roles, domain.ProjectGrantRolePrefix, c.zitadelRoles)) > 0 { + return nil, caos_errs.ThrowInvalidArgument(nil, "PROJECT-m9gKK", "Errors.Project.Grant.Member.Invalid") } err := c.checkUserExists(ctx, member.UserID, "") if err != nil { @@ -25,12 +28,12 @@ func (c *Commands) AddProjectGrantMember(ctx context.Context, member *domain.Pro return nil, err } if addedMember.State == domain.MemberStateActive { - return nil, caos_errs.ThrowPreconditionFailed(nil, "PROJECT-16dVN", "Errors.Project.Member.AlreadyExists") + return nil, caos_errs.ThrowAlreadyExists(nil, "PROJECT-16dVN", "Errors.Project.Member.AlreadyExists") } projectAgg := ProjectAggregateFromWriteModel(&addedMember.WriteModel) pushedEvents, err := c.eventstore.PushEvents( ctx, - project.NewProjectGrantMemberAddedEvent(ctx, projectAgg, member.AggregateID, member.UserID, member.GrantID, member.Roles...)) + project.NewProjectGrantMemberAddedEvent(ctx, projectAgg, member.UserID, member.GrantID, member.Roles...)) if err != nil { return nil, err } @@ -43,12 +46,12 @@ func (c *Commands) AddProjectGrantMember(ctx context.Context, member *domain.Pro } //ChangeProjectGrantMember updates an existing member -func (c *Commands) ChangeProjectGrantMember(ctx context.Context, member *domain.ProjectGrantMember, resourceOwner string) (*domain.ProjectGrantMember, error) { +func (c *Commands) ChangeProjectGrantMember(ctx context.Context, member *domain.ProjectGrantMember) (*domain.ProjectGrantMember, error) { if !member.IsValid() { - return nil, caos_errs.ThrowPreconditionFailed(nil, "PROJECT-109fs", "Errors.Project.Member.Invalid") + return nil, caos_errs.ThrowInvalidArgument(nil, "PROJECT-109fs", "Errors.Project.Member.Invalid") } if len(domain.CheckForInvalidRoles(member.Roles, domain.ProjectGrantRolePrefix, c.zitadelRoles)) > 0 { - return nil, caos_errs.ThrowPreconditionFailed(nil, "PROJECT-m0sDf", "Errors.Project.Member.Invalid") + return nil, caos_errs.ThrowInvalidArgument(nil, "PROJECT-m0sDf", "Errors.Project.Member.Invalid") } existingMember, err := c.projectGrantMemberWriteModelByID(ctx, member.AggregateID, member.UserID, member.GrantID) @@ -74,14 +77,17 @@ func (c *Commands) ChangeProjectGrantMember(ctx context.Context, member *domain. return memberWriteModelToProjectGrantMember(existingMember), nil } -func (c *Commands) RemoveProjectGrantMember(ctx context.Context, projectID, userID, grantID, resourceOwner string) (*domain.ObjectDetails, error) { +func (c *Commands) RemoveProjectGrantMember(ctx context.Context, projectID, userID, grantID string) (*domain.ObjectDetails, error) { + if projectID == "" || userID == "" || grantID == "" { + return nil, caos_errs.ThrowInvalidArgument(nil, "PROJECT-66mHd", "Errors.Project.Member.Invalid") + } m, err := c.projectGrantMemberWriteModelByID(ctx, projectID, userID, grantID) if err != nil { return nil, err } projectAgg := ProjectAggregateFromWriteModel(&m.WriteModel) - pushedEvents, err := c.eventstore.PushEvents(ctx, project.NewProjectGrantMemberRemovedEvent(ctx, projectAgg, projectID, userID, grantID)) + pushedEvents, err := c.eventstore.PushEvents(ctx, project.NewProjectGrantMemberRemovedEvent(ctx, projectAgg, userID, grantID)) if err != nil { return nil, err } diff --git a/internal/command/project_grant_member_test.go b/internal/command/project_grant_member_test.go new file mode 100644 index 000000000..79c1a638c --- /dev/null +++ b/internal/command/project_grant_member_test.go @@ -0,0 +1,646 @@ +package command + +import ( + "context" + "github.com/caos/zitadel/internal/api/authz" + "github.com/caos/zitadel/internal/domain" + caos_errs "github.com/caos/zitadel/internal/errors" + "github.com/caos/zitadel/internal/eventstore" + "github.com/caos/zitadel/internal/eventstore/repository" + "github.com/caos/zitadel/internal/eventstore/v1/models" + "github.com/caos/zitadel/internal/repository/project" + "github.com/caos/zitadel/internal/repository/user" + "github.com/stretchr/testify/assert" + "golang.org/x/text/language" + "testing" +) + +func TestCommandSide_AddProjectGrantMember(t *testing.T) { + type fields struct { + eventstore *eventstore.Eventstore + zitadelRoles []authz.RoleMapping + } + type args struct { + ctx context.Context + member *domain.ProjectGrantMember + } + type res struct { + want *domain.ProjectGrantMember + err func(error) bool + } + tests := []struct { + name string + fields fields + args args + res res + }{ + { + name: "invalid member, error", + fields: fields{ + eventstore: eventstoreExpect( + t, + ), + }, + args: args{ + ctx: context.Background(), + member: &domain.ProjectGrantMember{ + ObjectRoot: models.ObjectRoot{ + AggregateID: "project1", + }, + }, + }, + res: res{ + err: caos_errs.IsErrorInvalidArgument, + }, + }, + { + name: "invalid roles, error", + fields: fields{ + eventstore: eventstoreExpect( + t, + ), + }, + args: args{ + ctx: context.Background(), + member: &domain.ProjectGrantMember{ + ObjectRoot: models.ObjectRoot{ + AggregateID: "project1", + }, + GrantID: "projectgrant1", + UserID: "user1", + Roles: []string{"PROJECT_GRANT_OWNER"}, + }, + }, + res: res{ + err: caos_errs.IsErrorInvalidArgument, + }, + }, + { + name: "user not existing, precondition error", + fields: fields{ + eventstore: eventstoreExpect( + t, + expectFilter(), + ), + zitadelRoles: []authz.RoleMapping{ + { + Role: "PROJECT_GRANT_OWNER", + }, + }, + }, + args: args{ + ctx: context.Background(), + member: &domain.ProjectGrantMember{ + ObjectRoot: models.ObjectRoot{ + AggregateID: "project1", + }, + GrantID: "projectgrant1", + UserID: "user1", + Roles: []string{"PROJECT_GRANT_OWNER"}, + }, + }, + res: res{ + err: caos_errs.IsPreconditionFailed, + }, + }, + { + name: "member already exists, precondition error", + fields: fields{ + eventstore: eventstoreExpect( + t, + 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.NewProjectGrantMemberAddedEvent(context.Background(), + &project.NewAggregate("project1", "org1").Aggregate, + "user1", + "projectgrant1", + ), + ), + ), + ), + zitadelRoles: []authz.RoleMapping{ + { + Role: "PROJECT_GRANT_OWNER", + }, + }, + }, + args: args{ + ctx: context.Background(), + member: &domain.ProjectGrantMember{ + ObjectRoot: models.ObjectRoot{ + AggregateID: "project1", + }, + GrantID: "projectgrant1", + UserID: "user1", + Roles: []string{"PROJECT_GRANT_OWNER"}, + }, + }, + res: res{ + err: caos_errs.IsErrorAlreadyExists, + }, + }, + { + name: "member add uniqueconstraint err, already exists", + fields: fields{ + eventstore: eventstoreExpect( + t, + expectFilter( + eventFromEventPusher( + user.NewHumanAddedEvent(context.Background(), + &user.NewAggregate("user1", "org1").Aggregate, + "username1", + "firstname1", + "lastname1", + "nickname1", + "displayname1", + language.German, + domain.GenderMale, + "email1", + true, + ), + ), + ), + expectFilter(), + expectPushFailed(caos_errs.ThrowAlreadyExists(nil, "ERROR", "internal"), + []*repository.Event{ + eventFromEventPusher(project.NewProjectGrantMemberAddedEvent(context.Background(), + &project.NewAggregate("project1", "").Aggregate, + "user1", + "projectgrant1", + []string{"PROJECT_GRANT_OWNER"}..., + )), + }, + uniqueConstraintsFromEventConstraint(project.NewAddProjectGrantMemberUniqueConstraint("project1", "user1", "projectgrant1")), + ), + ), + zitadelRoles: []authz.RoleMapping{ + { + Role: "PROJECT_GRANT_OWNER", + }, + }, + }, + args: args{ + ctx: context.Background(), + member: &domain.ProjectGrantMember{ + ObjectRoot: models.ObjectRoot{ + AggregateID: "project1", + }, + GrantID: "projectgrant1", + UserID: "user1", + Roles: []string{"PROJECT_GRANT_OWNER"}, + }, + }, + res: res{ + err: caos_errs.IsErrorAlreadyExists, + }, + }, + { + name: "member add, ok", + fields: fields{ + eventstore: eventstoreExpect( + t, + expectFilter( + eventFromEventPusher( + user.NewHumanAddedEvent(context.Background(), + &user.NewAggregate("user1", "org1").Aggregate, + "username1", + "firstname1", + "lastname1", + "nickname1", + "displayname1", + language.German, + domain.GenderMale, + "email1", + true, + ), + ), + ), + expectFilter(), + expectPush( + []*repository.Event{ + eventFromEventPusher(project.NewProjectGrantMemberAddedEvent(context.Background(), + &project.NewAggregate("project1", "").Aggregate, + "user1", + "projectgrant1", + []string{"PROJECT_GRANT_OWNER"}..., + )), + }, + uniqueConstraintsFromEventConstraint(project.NewAddProjectGrantMemberUniqueConstraint("project1", "user1", "projectgrant1")), + ), + ), + zitadelRoles: []authz.RoleMapping{ + { + Role: "PROJECT_GRANT_OWNER", + }, + }, + }, + args: args{ + ctx: context.Background(), + member: &domain.ProjectGrantMember{ + ObjectRoot: models.ObjectRoot{ + AggregateID: "project1", + }, + UserID: "user1", + GrantID: "projectgrant1", + Roles: []string{"PROJECT_GRANT_OWNER"}, + }, + }, + res: res{ + want: &domain.ProjectGrantMember{ + ObjectRoot: models.ObjectRoot{ + AggregateID: "project1", + }, + GrantID: "projectgrant1", + UserID: "user1", + Roles: []string{"PROJECT_GRANT_OWNER"}, + }, + }, + }, + } + 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.AddProjectGrantMember(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_ChangeProjectGrantMember(t *testing.T) { + type fields struct { + eventstore *eventstore.Eventstore + zitadelRoles []authz.RoleMapping + } + type args struct { + ctx context.Context + member *domain.ProjectGrantMember + } + type res struct { + want *domain.ProjectGrantMember + err func(error) bool + } + tests := []struct { + name string + fields fields + args args + res res + }{ + { + name: "invalid member, error", + fields: fields{ + eventstore: eventstoreExpect( + t, + ), + }, + args: args{ + ctx: context.Background(), + member: &domain.ProjectGrantMember{ + ObjectRoot: models.ObjectRoot{ + AggregateID: "project1", + }, + }, + }, + res: res{ + err: caos_errs.IsErrorInvalidArgument, + }, + }, + { + name: "invalid roles, error", + fields: fields{ + eventstore: eventstoreExpect( + t, + ), + }, + args: args{ + ctx: context.Background(), + member: &domain.ProjectGrantMember{ + ObjectRoot: models.ObjectRoot{ + AggregateID: "project1", + }, + GrantID: "projectgrant1", + UserID: "user1", + Roles: []string{"PROJECT_OWNER"}, + }, + }, + res: res{ + err: caos_errs.IsErrorInvalidArgument, + }, + }, + { + name: "member not existing, not found error", + fields: fields{ + eventstore: eventstoreExpect( + t, + expectFilter(), + ), + zitadelRoles: []authz.RoleMapping{ + { + Role: "PROJECT_GRANT_OWNER", + }, + }, + }, + args: args{ + ctx: context.Background(), + member: &domain.ProjectGrantMember{ + ObjectRoot: models.ObjectRoot{ + AggregateID: "project1", + }, + GrantID: "projectgrant1", + UserID: "user1", + Roles: []string{"PROJECT_GRANT_OWNER"}, + }, + }, + res: res{ + err: caos_errs.IsNotFound, + }, + }, + { + name: "member not changed, precondition error", + fields: fields{ + eventstore: eventstoreExpect( + t, + expectFilter( + eventFromEventPusher( + project.NewProjectGrantMemberAddedEvent(context.Background(), + &project.NewAggregate("project1", "org1").Aggregate, + "user1", + "projectgrant1", + []string{"PROJECT_GRANT_OWNER"}..., + ), + ), + ), + ), + zitadelRoles: []authz.RoleMapping{ + { + Role: "PROJECT_GRANT_OWNER", + }, + }, + }, + args: args{ + ctx: context.Background(), + member: &domain.ProjectGrantMember{ + ObjectRoot: models.ObjectRoot{ + AggregateID: "project1", + }, + GrantID: "projectgrant1", + UserID: "user1", + Roles: []string{"PROJECT_GRANT_OWNER"}, + }, + }, + res: res{ + err: caos_errs.IsPreconditionFailed, + }, + }, + { + name: "member change, ok", + fields: fields{ + eventstore: eventstoreExpect( + t, + expectFilter( + eventFromEventPusher( + project.NewProjectGrantMemberAddedEvent(context.Background(), + &project.NewAggregate("project1", "org1").Aggregate, + "user1", + "projectgrant1", + []string{"PROJECT_GRANT_OWNER"}..., + ), + ), + ), + expectPush( + []*repository.Event{ + eventFromEventPusher(project.NewProjectGrantMemberChangedEvent(context.Background(), + &project.NewAggregate("project1", "org1").Aggregate, + "user1", + "projectgrant1", + []string{"PROJECT_GRANT_OWNER", "PROJECT_GRANT_VIEWER"}..., + )), + }, + ), + ), + zitadelRoles: []authz.RoleMapping{ + { + Role: "PROJECT_GRANT_OWNER", + }, + { + Role: "PROJECT_GRANT_VIEWER", + }, + }, + }, + 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"}, + }, + }, + res: res{ + want: &domain.ProjectGrantMember{ + ObjectRoot: models.ObjectRoot{ + ResourceOwner: "org1", + AggregateID: "project1", + }, + GrantID: "projectgrant1", + UserID: "user1", + Roles: []string{"PROJECT_GRANT_OWNER", "PROJECT_GRANT_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.ChangeProjectGrantMember(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_RemoveProjectGrantMember(t *testing.T) { + type fields struct { + eventstore *eventstore.Eventstore + } + type args struct { + ctx context.Context + projectID string + grantID 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 projectid missing, error", + fields: fields{ + eventstore: eventstoreExpect( + t, + ), + }, + args: args{ + ctx: context.Background(), + projectID: "", + userID: "user1", + grantID: "projectgrant1", + }, + res: res{ + err: caos_errs.IsErrorInvalidArgument, + }, + }, + { + name: "invalid member userid missing, error", + fields: fields{ + eventstore: eventstoreExpect( + t, + ), + }, + args: args{ + ctx: context.Background(), + projectID: "project1", + userID: "", + grantID: "projectgrant1", + }, + res: res{ + err: caos_errs.IsErrorInvalidArgument, + }, + }, + { + name: "invalid member grantid missing, error", + fields: fields{ + eventstore: eventstoreExpect( + t, + ), + }, + args: args{ + ctx: context.Background(), + projectID: "project1", + userID: "user1", + grantID: "", + }, + res: res{ + err: caos_errs.IsErrorInvalidArgument, + }, + }, + { + name: "member not existing, not found err", + fields: fields{ + eventstore: eventstoreExpect( + t, + expectFilter(), + ), + }, + args: args{ + ctx: context.Background(), + projectID: "project1", + userID: "user1", + grantID: "projectgrant1", + }, + res: res{ + err: caos_errs.IsNotFound, + }, + }, + { + name: "member remove, ok", + fields: fields{ + eventstore: eventstoreExpect( + t, + expectFilter( + eventFromEventPusher( + project.NewProjectGrantMemberAddedEvent(context.Background(), + &project.NewAggregate("project1", "org1").Aggregate, + "user1", + "projectgrant1", + []string{"PROJECT_OWNER"}..., + ), + ), + ), + expectPush( + []*repository.Event{ + eventFromEventPusher(project.NewProjectGrantMemberRemovedEvent(context.Background(), + &project.NewAggregate("project1", "org1").Aggregate, + "user1", + "projectgrant1", + )), + }, + uniqueConstraintsFromEventConstraint(project.NewRemoveProjectGrantMemberUniqueConstraint("project1", "user1", "projectgrant1")), + ), + ), + }, + args: args{ + ctx: context.Background(), + projectID: "project1", + userID: "user1", + grantID: "projectgrant1", + }, + res: res{ + want: &domain.ObjectDetails{ + ResourceOwner: "org1", + }, + }, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + r := &Commands{ + eventstore: tt.fields.eventstore, + } + got, err := r.RemoveProjectGrantMember(tt.args.ctx, tt.args.projectID, tt.args.userID, tt.args.grantID) + 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) + } + }) + } +} diff --git a/internal/command/project_grant_model.go b/internal/command/project_grant_model.go index e4b408162..3b3420bc0 100644 --- a/internal/command/project_grant_model.go +++ b/internal/command/project_grant_model.go @@ -3,6 +3,7 @@ package command import ( "github.com/caos/zitadel/internal/domain" "github.com/caos/zitadel/internal/eventstore" + "github.com/caos/zitadel/internal/repository/org" "github.com/caos/zitadel/internal/repository/project" ) @@ -105,3 +106,60 @@ func (wm *ProjectGrantWriteModel) Query() *eventstore.SearchQueryBuilder { } return query } + +type ProjectGrantPreConditionReadModel struct { + eventstore.WriteModel + + ProjectID string + GrantedOrgID string + ProjectExists bool + GrantedOrgExists bool + ExistingRoleKeys []string +} + +func NewProjectGrantPreConditionReadModel(projectID, grantedOrgID string) *ProjectGrantPreConditionReadModel { + return &ProjectGrantPreConditionReadModel{ + ProjectID: projectID, + GrantedOrgID: grantedOrgID, + } +} + +func (wm *ProjectGrantPreConditionReadModel) Reduce() error { + for _, event := range wm.Events { + switch e := event.(type) { + case *project.ProjectAddedEvent: + wm.ProjectExists = true + case *project.ProjectRemovedEvent: + wm.ProjectExists = false + case *project.RoleAddedEvent: + wm.ExistingRoleKeys = append(wm.ExistingRoleKeys, e.Key) + case *project.RoleRemovedEvent: + for i, key := range wm.ExistingRoleKeys { + if key == e.Key { + copy(wm.ExistingRoleKeys[i:], wm.ExistingRoleKeys[i+1:]) + wm.ExistingRoleKeys[len(wm.ExistingRoleKeys)-1] = "" + wm.ExistingRoleKeys = wm.ExistingRoleKeys[:len(wm.ExistingRoleKeys)-1] + continue + } + } + case *org.OrgAddedEvent: + wm.GrantedOrgExists = true + case *org.OrgRemovedEvent: + wm.GrantedOrgExists = false + } + } + return wm.WriteModel.Reduce() +} + +func (wm *ProjectGrantPreConditionReadModel) Query() *eventstore.SearchQueryBuilder { + query := eventstore.NewSearchQueryBuilder(eventstore.ColumnsEvent, org.AggregateType, project.AggregateType). + AggregateIDs(wm.ProjectID, wm.GrantedOrgID). + EventTypes( + org.OrgAddedEventType, + org.OrgRemovedEventType, + project.ProjectAddedType, + project.ProjectRemovedType, + project.RoleAddedType, + project.RoleRemovedType) + return query +} diff --git a/internal/command/project_grant_test.go b/internal/command/project_grant_test.go new file mode 100644 index 000000000..4274622b5 --- /dev/null +++ b/internal/command/project_grant_test.go @@ -0,0 +1,1396 @@ +package command + +import ( + "context" + "github.com/caos/zitadel/internal/domain" + caos_errs "github.com/caos/zitadel/internal/errors" + "github.com/caos/zitadel/internal/eventstore" + "github.com/caos/zitadel/internal/eventstore/repository" + "github.com/caos/zitadel/internal/eventstore/v1/models" + "github.com/caos/zitadel/internal/id" + id_mock "github.com/caos/zitadel/internal/id/mock" + "github.com/caos/zitadel/internal/repository/org" + "github.com/caos/zitadel/internal/repository/project" + "github.com/caos/zitadel/internal/repository/usergrant" + "github.com/stretchr/testify/assert" + "testing" +) + +func TestCommandSide_AddProjectGrant(t *testing.T) { + type fields struct { + eventstore *eventstore.Eventstore + idGenerator id.Generator + } + type args struct { + ctx context.Context + projectGrant *domain.ProjectGrant + resourceOwner string + } + type res struct { + want *domain.ProjectGrant + err func(error) bool + } + tests := []struct { + name string + fields fields + args args + res res + }{ + { + name: "invalid usergrant, error", + fields: fields{ + eventstore: eventstoreExpect( + t, + ), + }, + args: args{ + ctx: context.Background(), + projectGrant: &domain.ProjectGrant{ + ObjectRoot: models.ObjectRoot{ + AggregateID: "project1", + }, + }, + resourceOwner: "org1", + }, + res: res{ + err: caos_errs.IsErrorInvalidArgument, + }, + }, + { + name: "project not existing, precondition error", + fields: fields{ + eventstore: eventstoreExpect( + t, + expectFilter(), + ), + }, + args: args{ + ctx: context.Background(), + projectGrant: &domain.ProjectGrant{ + ObjectRoot: models.ObjectRoot{ + AggregateID: "project1", + }, + GrantedOrgID: "grantedorg1", + }, + resourceOwner: "org1", + }, + res: res{ + err: caos_errs.IsPreconditionFailed, + }, + }, + { + name: "granted org not existing, precondition error", + fields: fields{ + eventstore: eventstoreExpect( + t, + expectFilter( + eventFromEventPusher( + project.NewProjectAddedEvent(context.Background(), + &project.NewAggregate("project1", "org1").Aggregate, + "projectname1", + ), + ), + ), + ), + }, + args: args{ + ctx: context.Background(), + projectGrant: &domain.ProjectGrant{ + ObjectRoot: models.ObjectRoot{ + AggregateID: "project1", + }, + GrantedOrgID: "grantedorg1", + }, + resourceOwner: "org1", + }, + res: res{ + err: caos_errs.IsPreconditionFailed, + }, + }, + { + name: "project roles not existing, precondition error", + fields: fields{ + eventstore: eventstoreExpect( + t, + expectFilter( + eventFromEventPusher( + project.NewProjectAddedEvent(context.Background(), + &project.NewAggregate("project1", "org1").Aggregate, + "projectname1", + ), + ), + eventFromEventPusher( + org.NewOrgAddedEvent(context.Background(), + &org.NewAggregate("grantedorg1", "grantedorg1").Aggregate, + "granted org", + ), + ), + ), + ), + }, + args: args{ + ctx: context.Background(), + projectGrant: &domain.ProjectGrant{ + ObjectRoot: models.ObjectRoot{ + AggregateID: "project1", + }, + GrantedOrgID: "grantedorg1", + RoleKeys: []string{"key1"}, + }, + resourceOwner: "org1", + }, + res: res{ + err: caos_errs.IsPreconditionFailed, + }, + }, + { + name: "usergrant for project, ok", + fields: fields{ + eventstore: eventstoreExpect( + t, + expectFilter( + eventFromEventPusher( + project.NewProjectAddedEvent(context.Background(), + &project.NewAggregate("project1", "org1").Aggregate, + "projectname1", + ), + ), + eventFromEventPusher( + org.NewOrgAddedEvent(context.Background(), + &org.NewAggregate("grantedorg1", "grantedorg1").Aggregate, + "granted org", + ), + ), + eventFromEventPusher( + project.NewRoleAddedEvent(context.Background(), + &project.NewAggregate("project1", "org1").Aggregate, + "key1", + "key", + "", + ), + ), + ), + expectPush( + []*repository.Event{ + eventFromEventPusher(project.NewGrantAddedEvent(context.Background(), + &project.NewAggregate("project1", "org1").Aggregate, + "projectgrant1", + "grantedorg1", + []string{"key1"}, + )), + }, + uniqueConstraintsFromEventConstraint(project.NewAddProjectGrantUniqueConstraint("grantedorg1", "project1")), + ), + ), + idGenerator: id_mock.NewIDGeneratorExpectIDs(t, "projectgrant1"), + }, + args: args{ + ctx: context.Background(), + projectGrant: &domain.ProjectGrant{ + ObjectRoot: models.ObjectRoot{ + AggregateID: "project1", + }, + GrantedOrgID: "grantedorg1", + RoleKeys: []string{"key1"}, + }, + resourceOwner: "org1", + }, + res: res{ + want: &domain.ProjectGrant{ + ObjectRoot: models.ObjectRoot{ + AggregateID: "project1", + ResourceOwner: "org1", + }, + GrantID: "projectgrant1", + GrantedOrgID: "grantedorg1", + RoleKeys: []string{"key1"}, + State: domain.ProjectGrantStateActive, + }, + }, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + r := &Commands{ + eventstore: tt.fields.eventstore, + idGenerator: tt.fields.idGenerator, + } + got, err := r.AddProjectGrant(tt.args.ctx, tt.args.projectGrant, tt.args.resourceOwner) + 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_ChangeProjectGrant(t *testing.T) { + type fields struct { + eventstore *eventstore.Eventstore + } + type args struct { + ctx context.Context + projectGrant *domain.ProjectGrant + resourceOwner string + cascadeUserGrantIDs []string + } + type res struct { + want *domain.ProjectGrant + err func(error) bool + } + tests := []struct { + name string + fields fields + args args + res res + }{ + { + name: "invalid projectgrant, error", + fields: fields{ + eventstore: eventstoreExpect( + t, + ), + }, + args: args{ + ctx: context.Background(), + projectGrant: &domain.ProjectGrant{ + ObjectRoot: models.ObjectRoot{ + AggregateID: "project1", + }, + }, + resourceOwner: "org1", + }, + res: res{ + err: caos_errs.IsErrorInvalidArgument, + }, + }, + { + name: "projectgrant not existing, not found error", + fields: fields{ + eventstore: eventstoreExpect( + t, + expectFilter(), + ), + }, + args: args{ + ctx: context.Background(), + projectGrant: &domain.ProjectGrant{ + ObjectRoot: models.ObjectRoot{ + AggregateID: "project1", + }, + GrantID: "projectgrant1", + GrantedOrgID: "grantedorg1", + RoleKeys: []string{"key1"}, + }, + resourceOwner: "org1", + }, + res: res{ + err: caos_errs.IsNotFound, + }, + }, + { + name: "project not existing, precondition error", + fields: fields{ + eventstore: eventstoreExpect( + t, + expectFilter( + eventFromEventPusher(project.NewGrantAddedEvent(context.Background(), + &project.NewAggregate("project1", "org1").Aggregate, + "projectgrant1", + "grantedorg1", + []string{"key1"}, + )), + ), + expectFilter(), + ), + }, + args: args{ + ctx: context.Background(), + projectGrant: &domain.ProjectGrant{ + ObjectRoot: models.ObjectRoot{ + AggregateID: "project1", + }, + GrantID: "projectgrant1", + GrantedOrgID: "grantedorg1", + }, + resourceOwner: "org1", + }, + res: res{ + err: caos_errs.IsPreconditionFailed, + }, + }, + { + name: "granted org not existing, precondition error", + fields: fields{ + eventstore: eventstoreExpect( + t, + expectFilter( + eventFromEventPusher(project.NewGrantAddedEvent(context.Background(), + &project.NewAggregate("project1", "org1").Aggregate, + "projectgrant1", + "grantedorg1", + []string{"key1"}, + )), + ), + expectFilter( + eventFromEventPusher( + project.NewProjectAddedEvent(context.Background(), + &project.NewAggregate("project1", "org1").Aggregate, + "projectname1", + ), + ), + ), + ), + }, + args: args{ + ctx: context.Background(), + projectGrant: &domain.ProjectGrant{ + ObjectRoot: models.ObjectRoot{ + AggregateID: "project1", + }, + GrantID: "projectgrant1", + GrantedOrgID: "grantedorg1", + }, + resourceOwner: "org1", + }, + res: res{ + err: caos_errs.IsPreconditionFailed, + }, + }, + { + name: "project roles not existing, precondition error", + fields: fields{ + eventstore: eventstoreExpect( + t, + expectFilter( + eventFromEventPusher(project.NewGrantAddedEvent(context.Background(), + &project.NewAggregate("project1", "org1").Aggregate, + "projectgrant1", + "grantedorg1", + []string{"key1"}, + )), + ), + expectFilter( + eventFromEventPusher( + project.NewProjectAddedEvent(context.Background(), + &project.NewAggregate("project1", "org1").Aggregate, + "projectname1", + ), + ), + eventFromEventPusher( + org.NewOrgAddedEvent(context.Background(), + &org.NewAggregate("grantedorg1", "grantedorg1").Aggregate, + "granted org", + ), + ), + ), + ), + }, + args: args{ + ctx: context.Background(), + projectGrant: &domain.ProjectGrant{ + ObjectRoot: models.ObjectRoot{ + AggregateID: "project1", + }, + GrantID: "projectgrant1", + GrantedOrgID: "grantedorg1", + RoleKeys: []string{"key1"}, + }, + resourceOwner: "org1", + }, + res: res{ + err: caos_errs.IsPreconditionFailed, + }, + }, + { + name: "projectgrant not changed, precondition error", + fields: fields{ + eventstore: eventstoreExpect( + t, + expectFilter( + eventFromEventPusher(project.NewGrantAddedEvent(context.Background(), + &project.NewAggregate("project1", "org1").Aggregate, + "projectgrant1", + "grantedorg1", + []string{"key1"}, + )), + ), + expectFilter( + eventFromEventPusher( + project.NewProjectAddedEvent(context.Background(), + &project.NewAggregate("project1", "org1").Aggregate, + "projectname1", + ), + ), + eventFromEventPusher( + org.NewOrgAddedEvent(context.Background(), + &org.NewAggregate("grantedorg1", "grantedorg1").Aggregate, + "granted org", + ), + ), + eventFromEventPusher( + project.NewRoleAddedEvent(context.Background(), + &project.NewAggregate("project1", "org1").Aggregate, + "key1", + "key", + "", + ), + ), + ), + ), + }, + args: args{ + ctx: context.Background(), + projectGrant: &domain.ProjectGrant{ + ObjectRoot: models.ObjectRoot{ + AggregateID: "project1", + }, + GrantID: "projectgrant1", + GrantedOrgID: "grantedorg1", + RoleKeys: []string{"key1"}, + }, + resourceOwner: "org1", + }, + res: res{ + err: caos_errs.IsPreconditionFailed, + }, + }, + { + name: "projectgrant only added roles, ok", + fields: fields{ + eventstore: eventstoreExpect( + t, + expectFilter( + eventFromEventPusher(project.NewGrantAddedEvent(context.Background(), + &project.NewAggregate("project1", "org1").Aggregate, + "projectgrant1", + "grantedorg1", + []string{"key1"}, + )), + ), + expectFilter( + eventFromEventPusher( + project.NewProjectAddedEvent(context.Background(), + &project.NewAggregate("project1", "org1").Aggregate, + "projectname1", + ), + ), + eventFromEventPusher( + org.NewOrgAddedEvent(context.Background(), + &org.NewAggregate("grantedorg1", "grantedorg1").Aggregate, + "granted org", + ), + ), + eventFromEventPusher( + project.NewRoleAddedEvent(context.Background(), + &project.NewAggregate("project1", "org1").Aggregate, + "key1", + "key", + "", + ), + ), + eventFromEventPusher( + project.NewRoleAddedEvent(context.Background(), + &project.NewAggregate("project1", "org1").Aggregate, + "key2", + "key2", + "", + ), + ), + ), + expectPush( + []*repository.Event{ + eventFromEventPusher(project.NewGrantChangedEvent(context.Background(), + &project.NewAggregate("project1", "org1").Aggregate, + "projectgrant1", + []string{"key1", "key2"}, + )), + }, + ), + ), + }, + args: args{ + ctx: context.Background(), + projectGrant: &domain.ProjectGrant{ + ObjectRoot: models.ObjectRoot{ + AggregateID: "project1", + }, + GrantID: "projectgrant1", + GrantedOrgID: "grantedorg1", + RoleKeys: []string{"key1", "key2"}, + }, + resourceOwner: "org1", + }, + res: res{ + want: &domain.ProjectGrant{ + ObjectRoot: models.ObjectRoot{ + AggregateID: "project1", + ResourceOwner: "org1", + }, + GrantID: "projectgrant1", + GrantedOrgID: "grantedorg1", + RoleKeys: []string{"key1", "key2"}, + State: domain.ProjectGrantStateActive, + }, + }, + }, + { + name: "projectgrant remove roles, usergrant not found, ok", + fields: fields{ + eventstore: eventstoreExpect( + t, + expectFilter( + eventFromEventPusher(project.NewGrantAddedEvent(context.Background(), + &project.NewAggregate("project1", "org1").Aggregate, + "projectgrant1", + "grantedorg1", + []string{"key1", "key2"}, + )), + ), + expectFilter( + eventFromEventPusher( + project.NewProjectAddedEvent(context.Background(), + &project.NewAggregate("project1", "org1").Aggregate, + "projectname1", + ), + ), + eventFromEventPusher( + org.NewOrgAddedEvent(context.Background(), + &org.NewAggregate("grantedorg1", "grantedorg1").Aggregate, + "granted org", + ), + ), + eventFromEventPusher( + project.NewRoleAddedEvent(context.Background(), + &project.NewAggregate("project1", "org1").Aggregate, + "key1", + "key", + "", + ), + ), + eventFromEventPusher( + project.NewRoleAddedEvent(context.Background(), + &project.NewAggregate("project1", "org1").Aggregate, + "key2", + "key2", + "", + ), + ), + ), + expectFilter(), + expectPush( + []*repository.Event{ + eventFromEventPusher(project.NewGrantChangedEvent(context.Background(), + &project.NewAggregate("project1", "org1").Aggregate, + "projectgrant1", + []string{"key1"}, + )), + }, + ), + ), + }, + args: args{ + ctx: context.Background(), + projectGrant: &domain.ProjectGrant{ + ObjectRoot: models.ObjectRoot{ + AggregateID: "project1", + }, + GrantID: "projectgrant1", + GrantedOrgID: "grantedorg1", + RoleKeys: []string{"key1"}, + }, + resourceOwner: "org1", + cascadeUserGrantIDs: []string{"usergrant1"}, + }, + res: res{ + want: &domain.ProjectGrant{ + ObjectRoot: models.ObjectRoot{ + AggregateID: "project1", + ResourceOwner: "org1", + }, + GrantID: "projectgrant1", + GrantedOrgID: "grantedorg1", + RoleKeys: []string{"key1"}, + State: domain.ProjectGrantStateActive, + }, + }, + }, + { + name: "projectgrant remove roles, usergrant not found, ok", + fields: fields{ + eventstore: eventstoreExpect( + t, + expectFilter( + eventFromEventPusher(project.NewGrantAddedEvent(context.Background(), + &project.NewAggregate("project1", "org1").Aggregate, + "projectgrant1", + "grantedorg1", + []string{"key1", "key2"}, + )), + ), + expectFilter( + eventFromEventPusher( + project.NewProjectAddedEvent(context.Background(), + &project.NewAggregate("project1", "org1").Aggregate, + "projectname1", + ), + ), + eventFromEventPusher( + org.NewOrgAddedEvent(context.Background(), + &org.NewAggregate("grantedorg1", "grantedorg1").Aggregate, + "granted org", + ), + ), + eventFromEventPusher( + project.NewRoleAddedEvent(context.Background(), + &project.NewAggregate("project1", "org1").Aggregate, + "key1", + "key", + "", + ), + ), + eventFromEventPusher( + project.NewRoleAddedEvent(context.Background(), + &project.NewAggregate("project1", "org1").Aggregate, + "key2", + "key2", + "", + ), + ), + ), + expectFilter( + eventFromEventPusher( + usergrant.NewUserGrantAddedEvent(context.Background(), + &usergrant.NewAggregate("usergrant1", "org1").Aggregate, + "user1", + "project1", + "projectgrant1", + []string{"key1", "key2"}), + ), + ), + expectPush( + []*repository.Event{ + eventFromEventPusher(project.NewGrantChangedEvent(context.Background(), + &project.NewAggregate("project1", "org1").Aggregate, + "projectgrant1", + []string{"key1"}, + )), + eventFromEventPusher(usergrant.NewUserGrantCascadeChangedEvent(context.Background(), + &usergrant.NewAggregate("usergrant1", "org1").Aggregate, + []string{"key1"}, + )), + }, + ), + ), + }, + args: args{ + ctx: context.Background(), + projectGrant: &domain.ProjectGrant{ + ObjectRoot: models.ObjectRoot{ + AggregateID: "project1", + }, + GrantID: "projectgrant1", + GrantedOrgID: "grantedorg1", + RoleKeys: []string{"key1"}, + }, + resourceOwner: "org1", + cascadeUserGrantIDs: []string{"usergrant1"}, + }, + res: res{ + want: &domain.ProjectGrant{ + ObjectRoot: models.ObjectRoot{ + AggregateID: "project1", + ResourceOwner: "org1", + }, + GrantID: "projectgrant1", + GrantedOrgID: "grantedorg1", + RoleKeys: []string{"key1"}, + State: domain.ProjectGrantStateActive, + }, + }, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + r := &Commands{ + eventstore: tt.fields.eventstore, + } + got, err := r.ChangeProjectGrant(tt.args.ctx, tt.args.projectGrant, tt.args.resourceOwner, tt.args.cascadeUserGrantIDs...) + 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_DeactivateProjectGrant(t *testing.T) { + type fields struct { + eventstore *eventstore.Eventstore + } + type args struct { + ctx context.Context + projectID string + grantID string + resourceOwner string + } + type res struct { + want *domain.ObjectDetails + err func(error) bool + } + tests := []struct { + name string + fields fields + args args + res res + }{ + { + name: "missing projectid, invalid error", + fields: fields{ + eventstore: eventstoreExpect( + t, + ), + }, + args: args{ + ctx: context.Background(), + grantID: "projectgrant1", + resourceOwner: "org1", + }, + res: res{ + err: caos_errs.IsErrorInvalidArgument, + }, + }, + { + name: "missing grantid, invalid error", + fields: fields{ + eventstore: eventstoreExpect( + t, + ), + }, + args: args{ + ctx: context.Background(), + projectID: "project1", + resourceOwner: "org1", + }, + res: res{ + err: caos_errs.IsErrorInvalidArgument, + }, + }, + { + name: "project not existing, precondition failed error", + fields: fields{ + eventstore: eventstoreExpect( + t, + expectFilter(), + ), + }, + args: args{ + ctx: context.Background(), + projectID: "project1", + grantID: "projectgrant1", + resourceOwner: "org1", + }, + res: res{ + err: caos_errs.IsPreconditionFailed, + }, + }, + { + name: "projectgrant not existing, precondition error", + fields: fields{ + eventstore: eventstoreExpect( + t, + expectFilter( + eventFromEventPusher( + project.NewProjectAddedEvent(context.Background(), + &project.NewAggregate("project1", "org1").Aggregate, + "projectname1", + ), + ), + ), + expectFilter(), + ), + }, + args: args{ + ctx: context.Background(), + projectID: "project1", + grantID: "projectgrant1", + resourceOwner: "org1", + }, + res: res{ + err: caos_errs.IsNotFound, + }, + }, + { + name: "projectgrant already deactivated, precondition error", + fields: fields{ + eventstore: eventstoreExpect( + t, + expectFilter( + eventFromEventPusher( + project.NewProjectAddedEvent(context.Background(), + &project.NewAggregate("project1", "org1").Aggregate, + "projectname1", + ), + ), + ), + expectFilter( + eventFromEventPusher(project.NewGrantAddedEvent(context.Background(), + &project.NewAggregate("project1", "org1").Aggregate, + "projectgrant1", + "grantedorg1", + []string{"key1"}, + )), + eventFromEventPusher(project.NewGrantDeactivateEvent(context.Background(), + &project.NewAggregate("project1", "org1").Aggregate, + "projectgrant1", + )), + ), + ), + }, + args: args{ + ctx: context.Background(), + projectID: "project1", + grantID: "projectgrant1", + resourceOwner: "org1", + }, + res: res{ + err: caos_errs.IsPreconditionFailed, + }, + }, + { + name: "projectgrant deactivate, ok", + fields: fields{ + eventstore: eventstoreExpect( + t, + expectFilter( + eventFromEventPusher( + project.NewProjectAddedEvent(context.Background(), + &project.NewAggregate("project1", "org1").Aggregate, + "projectname1", + ), + ), + ), + expectFilter( + eventFromEventPusher(project.NewGrantAddedEvent(context.Background(), + &project.NewAggregate("project1", "org1").Aggregate, + "projectgrant1", + "grantedorg1", + []string{"key1"}, + )), + ), + expectPush( + []*repository.Event{ + eventFromEventPusher(project.NewGrantDeactivateEvent(context.Background(), + &project.NewAggregate("project1", "org1").Aggregate, + "projectgrant1", + )), + }, + ), + ), + }, + args: args{ + ctx: context.Background(), + projectID: "project1", + grantID: "projectgrant1", + resourceOwner: "org1", + }, + res: res{ + want: &domain.ObjectDetails{ + ResourceOwner: "org1", + }, + }, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + r := &Commands{ + eventstore: tt.fields.eventstore, + } + got, err := r.DeactivateProjectGrant(tt.args.ctx, tt.args.projectID, tt.args.grantID, tt.args.resourceOwner) + 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_ReactivateProjectGrant(t *testing.T) { + type fields struct { + eventstore *eventstore.Eventstore + } + type args struct { + ctx context.Context + projectID string + grantID string + resourceOwner string + } + type res struct { + want *domain.ObjectDetails + err func(error) bool + } + tests := []struct { + name string + fields fields + args args + res res + }{ + { + name: "missing projectid, invalid error", + fields: fields{ + eventstore: eventstoreExpect( + t, + ), + }, + args: args{ + ctx: context.Background(), + grantID: "projectgrant1", + resourceOwner: "org1", + }, + res: res{ + err: caos_errs.IsErrorInvalidArgument, + }, + }, + { + name: "missing grantid, invalid error", + fields: fields{ + eventstore: eventstoreExpect( + t, + ), + }, + args: args{ + ctx: context.Background(), + projectID: "project1", + resourceOwner: "org1", + }, + res: res{ + err: caos_errs.IsErrorInvalidArgument, + }, + }, + { + name: "project not existing, precondition failed error", + fields: fields{ + eventstore: eventstoreExpect( + t, + expectFilter(), + ), + }, + args: args{ + ctx: context.Background(), + projectID: "project1", + grantID: "projectgrant1", + resourceOwner: "org1", + }, + res: res{ + err: caos_errs.IsPreconditionFailed, + }, + }, + { + name: "projectgrant not existing, precondition error", + fields: fields{ + eventstore: eventstoreExpect( + t, + expectFilter( + eventFromEventPusher( + project.NewProjectAddedEvent(context.Background(), + &project.NewAggregate("project1", "org1").Aggregate, + "projectname1", + ), + ), + ), + expectFilter(), + ), + }, + args: args{ + ctx: context.Background(), + projectID: "project1", + grantID: "projectgrant1", + resourceOwner: "org1", + }, + res: res{ + err: caos_errs.IsNotFound, + }, + }, + { + name: "projectgrant not inactive, precondition error", + fields: fields{ + eventstore: eventstoreExpect( + t, + expectFilter( + eventFromEventPusher( + project.NewProjectAddedEvent(context.Background(), + &project.NewAggregate("project1", "org1").Aggregate, + "projectname1", + ), + ), + ), + expectFilter( + eventFromEventPusher(project.NewGrantAddedEvent(context.Background(), + &project.NewAggregate("project1", "org1").Aggregate, + "projectgrant1", + "grantedorg1", + []string{"key1"}, + )), + ), + ), + }, + args: args{ + ctx: context.Background(), + projectID: "project1", + grantID: "projectgrant1", + resourceOwner: "org1", + }, + res: res{ + err: caos_errs.IsPreconditionFailed, + }, + }, + { + name: "projectgrant reactivate, ok", + fields: fields{ + eventstore: eventstoreExpect( + t, + expectFilter( + eventFromEventPusher( + project.NewProjectAddedEvent(context.Background(), + &project.NewAggregate("project1", "org1").Aggregate, + "projectname1", + ), + ), + ), + expectFilter( + eventFromEventPusher(project.NewGrantAddedEvent(context.Background(), + &project.NewAggregate("project1", "org1").Aggregate, + "projectgrant1", + "grantedorg1", + []string{"key1"}, + )), + eventFromEventPusher(project.NewGrantDeactivateEvent(context.Background(), + &project.NewAggregate("project1", "org1").Aggregate, + "projectgrant1", + )), + ), + expectPush( + []*repository.Event{ + eventFromEventPusher(project.NewGrantReactivatedEvent(context.Background(), + &project.NewAggregate("project1", "org1").Aggregate, + "projectgrant1", + )), + }, + ), + ), + }, + args: args{ + ctx: context.Background(), + projectID: "project1", + grantID: "projectgrant1", + resourceOwner: "org1", + }, + res: res{ + want: &domain.ObjectDetails{ + ResourceOwner: "org1", + }, + }, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + r := &Commands{ + eventstore: tt.fields.eventstore, + } + got, err := r.ReactivateProjectGrant(tt.args.ctx, tt.args.projectID, tt.args.grantID, tt.args.resourceOwner) + 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_RemoveProjectGrant(t *testing.T) { + type fields struct { + eventstore *eventstore.Eventstore + } + type args struct { + ctx context.Context + projectID string + grantID string + resourceOwner string + cascadeUserGrantIDs []string + } + type res struct { + want *domain.ObjectDetails + err func(error) bool + } + tests := []struct { + name string + fields fields + args args + res res + }{ + { + name: "missing projectid, invalid error", + fields: fields{ + eventstore: eventstoreExpect( + t, + ), + }, + args: args{ + ctx: context.Background(), + grantID: "projectgrant1", + resourceOwner: "org1", + }, + res: res{ + err: caos_errs.IsErrorInvalidArgument, + }, + }, + { + name: "missing grantid, invalid error", + fields: fields{ + eventstore: eventstoreExpect( + t, + ), + }, + args: args{ + ctx: context.Background(), + projectID: "project1", + resourceOwner: "org1", + }, + res: res{ + err: caos_errs.IsErrorInvalidArgument, + }, + }, + { + name: "project not existing, precondition failed error", + fields: fields{ + eventstore: eventstoreExpect( + t, + expectFilter(), + ), + }, + args: args{ + ctx: context.Background(), + projectID: "project1", + grantID: "projectgrant1", + resourceOwner: "org1", + }, + res: res{ + err: caos_errs.IsPreconditionFailed, + }, + }, + { + name: "projectgrant not existing, precondition error", + fields: fields{ + eventstore: eventstoreExpect( + t, + expectFilter( + eventFromEventPusher( + project.NewProjectAddedEvent(context.Background(), + &project.NewAggregate("project1", "org1").Aggregate, + "projectname1", + ), + ), + ), + expectFilter(), + ), + }, + args: args{ + ctx: context.Background(), + projectID: "project1", + grantID: "projectgrant1", + resourceOwner: "org1", + }, + res: res{ + err: caos_errs.IsNotFound, + }, + }, + { + name: "projectgrant remove, ok", + fields: fields{ + eventstore: eventstoreExpect( + t, + expectFilter( + eventFromEventPusher( + project.NewProjectAddedEvent(context.Background(), + &project.NewAggregate("project1", "org1").Aggregate, + "projectname1", + ), + ), + ), + expectFilter( + eventFromEventPusher(project.NewGrantAddedEvent(context.Background(), + &project.NewAggregate("project1", "org1").Aggregate, + "projectgrant1", + "grantedorg1", + []string{"key1"}, + )), + ), + expectPush( + []*repository.Event{ + eventFromEventPusher(project.NewGrantRemovedEvent(context.Background(), + &project.NewAggregate("project1", "org1").Aggregate, + "projectgrant1", + "grantedorg1", + )), + }, + uniqueConstraintsFromEventConstraint(project.NewRemoveProjectGrantUniqueConstraint("grantedorg1", "project1")), + ), + ), + }, + args: args{ + ctx: context.Background(), + projectID: "project1", + grantID: "projectgrant1", + resourceOwner: "org1", + }, + res: res{ + want: &domain.ObjectDetails{ + ResourceOwner: "org1", + }, + }, + }, + { + name: "projectgrant remove, cascading usergrant not found, ok", + fields: fields{ + eventstore: eventstoreExpect( + t, + expectFilter( + eventFromEventPusher( + project.NewProjectAddedEvent(context.Background(), + &project.NewAggregate("project1", "org1").Aggregate, + "projectname1", + ), + ), + ), + expectFilter( + eventFromEventPusher(project.NewGrantAddedEvent(context.Background(), + &project.NewAggregate("project1", "org1").Aggregate, + "projectgrant1", + "grantedorg1", + []string{"key1"}, + )), + ), + expectFilter(), + expectPush( + []*repository.Event{ + eventFromEventPusher(project.NewGrantRemovedEvent(context.Background(), + &project.NewAggregate("project1", "org1").Aggregate, + "projectgrant1", + "grantedorg1", + )), + }, + uniqueConstraintsFromEventConstraint(project.NewRemoveProjectGrantUniqueConstraint("grantedorg1", "project1")), + ), + ), + }, + args: args{ + ctx: context.Background(), + projectID: "project1", + grantID: "projectgrant1", + resourceOwner: "org1", + cascadeUserGrantIDs: []string{"usergrant1"}, + }, + res: res{ + want: &domain.ObjectDetails{ + ResourceOwner: "org1", + }, + }, + }, + { + name: "projectgrant remove with cascading usergrants, ok", + fields: fields{ + eventstore: eventstoreExpect( + t, + expectFilter( + eventFromEventPusher( + project.NewProjectAddedEvent(context.Background(), + &project.NewAggregate("project1", "org1").Aggregate, + "projectname1", + ), + ), + ), + expectFilter( + eventFromEventPusher(project.NewGrantAddedEvent(context.Background(), + &project.NewAggregate("project1", "org1").Aggregate, + "projectgrant1", + "grantedorg1", + []string{"key1"}, + )), + ), + expectFilter( + eventFromEventPusher(usergrant.NewUserGrantAddedEvent(context.Background(), + &usergrant.NewAggregate("usergrant1", "org1").Aggregate, + "user1", + "project1", + "projectgrant1", + []string{"key1"}))), + expectPush( + []*repository.Event{ + eventFromEventPusher(project.NewGrantRemovedEvent(context.Background(), + &project.NewAggregate("project1", "org1").Aggregate, + "projectgrant1", + "grantedorg1", + )), + eventFromEventPusher(usergrant.NewUserGrantCascadeRemovedEvent(context.Background(), + &usergrant.NewAggregate("usergrant1", "org1").Aggregate, + "user1", + "project1", + "projectgrant1", + )), + }, + uniqueConstraintsFromEventConstraint(project.NewRemoveProjectGrantUniqueConstraint("grantedorg1", "project1")), + uniqueConstraintsFromEventConstraint(usergrant.NewRemoveUserGrantUniqueConstraint("org1", "user1", "project1", "projectgrant1")), + ), + ), + }, + args: args{ + ctx: context.Background(), + projectID: "project1", + grantID: "projectgrant1", + resourceOwner: "org1", + cascadeUserGrantIDs: []string{"usergrant1"}, + }, + res: res{ + want: &domain.ObjectDetails{ + ResourceOwner: "org1", + }, + }, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + r := &Commands{ + eventstore: tt.fields.eventstore, + } + got, err := r.RemoveProjectGrant(tt.args.ctx, tt.args.projectID, tt.args.grantID, tt.args.resourceOwner, tt.args.cascadeUserGrantIDs...) + 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) + } + }) + } +} diff --git a/internal/command/project_member.go b/internal/command/project_member.go index 7d1536b5f..749cb277c 100644 --- a/internal/command/project_member.go +++ b/internal/command/project_member.go @@ -34,10 +34,10 @@ func (c *Commands) AddProjectMember(ctx context.Context, member *domain.Member, func (c *Commands) addProjectMember(ctx context.Context, projectAgg *eventstore.Aggregate, addedMember *ProjectMemberWriteModel, member *domain.Member) (eventstore.EventPusher, error) { if !member.IsValid() { - return nil, caos_errs.ThrowPreconditionFailed(nil, "PROJECT-W8m4l", "Errors.Project.Member.Invalid") + return nil, caos_errs.ThrowInvalidArgument(nil, "PROJECT-W8m4l", "Errors.Project.Member.Invalid") } if len(domain.CheckForInvalidRoles(member.Roles, domain.ProjectRolePrefix, c.zitadelRoles)) > 0 { - return nil, caos_errs.ThrowPreconditionFailed(nil, "PROJECT-3m9ds", "Errors.Project.Member.Invalid") + return nil, caos_errs.ThrowInvalidArgument(nil, "PROJECT-3m9ds", "Errors.Project.Member.Invalid") } err := c.checkUserExists(ctx, addedMember.UserID, "") @@ -58,10 +58,10 @@ func (c *Commands) addProjectMember(ctx context.Context, projectAgg *eventstore. //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, caos_errs.ThrowPreconditionFailed(nil, "PROJECT-LiaZi", "Errors.Project.Member.Invalid") + return nil, caos_errs.ThrowInvalidArgument(nil, "PROJECT-LiaZi", "Errors.Project.Member.Invalid") } if len(domain.CheckForInvalidRoles(member.Roles, domain.ProjectRolePrefix, c.zitadelRoles)) > 0 { - return nil, caos_errs.ThrowPreconditionFailed(nil, "PROJECT-3m9d", "Errors.Project.Member.Invalid") + return nil, caos_errs.ThrowInvalidArgument(nil, "PROJECT-3m9d", "Errors.Project.Member.Invalid") } existingMember, err := c.projectMemberWriteModelByID(ctx, member.AggregateID, member.UserID, resourceOwner) @@ -87,6 +87,9 @@ func (c *Commands) ChangeProjectMember(ctx context.Context, member *domain.Membe } func (c *Commands) RemoveProjectMember(ctx context.Context, projectID, userID, resourceOwner string) (*domain.ObjectDetails, error) { + if projectID == "" || userID == "" { + return nil, caos_errs.ThrowInvalidArgument(nil, "PROJECT-66mHd", "Errors.Project.Member.Invalid") + } m, err := c.projectMemberWriteModelByID(ctx, projectID, userID, resourceOwner) if err != nil && !errors.IsNotFound(err) { return nil, err diff --git a/internal/command/project_member_test.go b/internal/command/project_member_test.go new file mode 100644 index 000000000..7921b9053 --- /dev/null +++ b/internal/command/project_member_test.go @@ -0,0 +1,625 @@ +package command + +import ( + "context" + "github.com/caos/zitadel/internal/api/authz" + "github.com/caos/zitadel/internal/domain" + caos_errs "github.com/caos/zitadel/internal/errors" + "github.com/caos/zitadel/internal/eventstore" + "github.com/caos/zitadel/internal/eventstore/repository" + "github.com/caos/zitadel/internal/eventstore/v1/models" + "github.com/caos/zitadel/internal/repository/member" + "github.com/caos/zitadel/internal/repository/project" + "github.com/caos/zitadel/internal/repository/user" + "github.com/stretchr/testify/assert" + "golang.org/x/text/language" + "testing" +) + +func TestCommandSide_AddProjectMember(t *testing.T) { + type fields struct { + eventstore *eventstore.Eventstore + zitadelRoles []authz.RoleMapping + } + type args struct { + ctx context.Context + member *domain.Member + resourceOwner string + } + type res struct { + want *domain.Member + err func(error) bool + } + tests := []struct { + name string + fields fields + args args + res res + }{ + { + name: "invalid member, error", + fields: fields{ + eventstore: eventstoreExpect( + t, + ), + }, + args: args{ + ctx: context.Background(), + member: &domain.Member{ + ObjectRoot: models.ObjectRoot{ + AggregateID: "project1", + }, + }, + resourceOwner: "org1", + }, + res: res{ + err: caos_errs.IsErrorInvalidArgument, + }, + }, + { + name: "invalid roles, error", + fields: fields{ + eventstore: eventstoreExpect( + t, + ), + }, + args: args{ + ctx: context.Background(), + member: &domain.Member{ + ObjectRoot: models.ObjectRoot{ + AggregateID: "project1", + }, + UserID: "user1", + Roles: []string{"PROJECT_OWNER"}, + }, + resourceOwner: "org1", + }, + res: res{ + err: caos_errs.IsErrorInvalidArgument, + }, + }, + { + name: "user not existing, precondition error", + fields: fields{ + eventstore: eventstoreExpect( + t, + expectFilter(), + ), + zitadelRoles: []authz.RoleMapping{ + { + Role: domain.RoleProjectOwner, + }, + }, + }, + args: args{ + ctx: context.Background(), + member: &domain.Member{ + ObjectRoot: models.ObjectRoot{ + AggregateID: "project1", + }, + UserID: "user1", + Roles: []string{"PROJECT_OWNER"}, + }, + resourceOwner: "org1", + }, + res: res{ + err: caos_errs.IsPreconditionFailed, + }, + }, + { + name: "member already exists, precondition error", + fields: fields{ + eventstore: eventstoreExpect( + t, + 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.NewProjectMemberAddedEvent(context.Background(), + &project.NewAggregate("project1", "org1").Aggregate, + "user1", + ), + ), + ), + ), + zitadelRoles: []authz.RoleMapping{ + { + Role: domain.RoleProjectOwner, + }, + }, + }, + args: args{ + ctx: context.Background(), + member: &domain.Member{ + ObjectRoot: models.ObjectRoot{ + AggregateID: "project1", + }, + UserID: "user1", + Roles: []string{"PROJECT_OWNER"}, + }, + resourceOwner: "org1", + }, + res: res{ + err: caos_errs.IsErrorAlreadyExists, + }, + }, + { + name: "member add uniqueconstraint err, already exists", + fields: fields{ + eventstore: eventstoreExpect( + t, + expectFilter( + eventFromEventPusher( + user.NewHumanAddedEvent(context.Background(), + &user.NewAggregate("user1", "org1").Aggregate, + "username1", + "firstname1", + "lastname1", + "nickname1", + "displayname1", + language.German, + domain.GenderMale, + "email1", + true, + ), + ), + ), + expectFilter(), + expectPushFailed(caos_errs.ThrowAlreadyExists(nil, "ERROR", "internal"), + []*repository.Event{ + eventFromEventPusher(project.NewProjectMemberAddedEvent(context.Background(), + &project.NewAggregate("project1", "org1").Aggregate, + "user1", + []string{"PROJECT_OWNER"}..., + )), + }, + uniqueConstraintsFromEventConstraint(member.NewAddMemberUniqueConstraint("project1", "user1")), + ), + ), + zitadelRoles: []authz.RoleMapping{ + { + Role: domain.RoleProjectOwner, + }, + }, + }, + args: args{ + ctx: context.Background(), + member: &domain.Member{ + ObjectRoot: models.ObjectRoot{ + AggregateID: "project1", + }, + UserID: "user1", + Roles: []string{"PROJECT_OWNER"}, + }, + resourceOwner: "org1", + }, + res: res{ + err: caos_errs.IsErrorAlreadyExists, + }, + }, + { + name: "member add, ok", + fields: fields{ + eventstore: eventstoreExpect( + t, + expectFilter( + eventFromEventPusher( + user.NewHumanAddedEvent(context.Background(), + &user.NewAggregate("user1", "org1").Aggregate, + "username1", + "firstname1", + "lastname1", + "nickname1", + "displayname1", + language.German, + domain.GenderMale, + "email1", + true, + ), + ), + ), + expectFilter(), + expectPush( + []*repository.Event{ + eventFromEventPusher(project.NewProjectMemberAddedEvent(context.Background(), + &project.NewAggregate("project1", "org1").Aggregate, + "user1", + []string{"PROJECT_OWNER"}..., + )), + }, + uniqueConstraintsFromEventConstraint(member.NewAddMemberUniqueConstraint("project1", "user1")), + ), + ), + zitadelRoles: []authz.RoleMapping{ + { + Role: domain.RoleProjectOwner, + }, + }, + }, + args: args{ + ctx: context.Background(), + member: &domain.Member{ + ObjectRoot: models.ObjectRoot{ + AggregateID: "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}, + }, + }, + }, + } + 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.AddProjectMember(tt.args.ctx, tt.args.member, tt.args.resourceOwner) + 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_ChangeProjectMember(t *testing.T) { + type fields struct { + eventstore *eventstore.Eventstore + zitadelRoles []authz.RoleMapping + } + type args struct { + ctx context.Context + member *domain.Member + resourceOwner string + } + type res struct { + want *domain.Member + err func(error) bool + } + tests := []struct { + name string + fields fields + args args + res res + }{ + { + name: "invalid member, error", + fields: fields{ + eventstore: eventstoreExpect( + t, + ), + }, + args: args{ + ctx: context.Background(), + member: &domain.Member{ + ObjectRoot: models.ObjectRoot{ + AggregateID: "project1", + }, + }, + resourceOwner: "org1", + }, + res: res{ + err: caos_errs.IsErrorInvalidArgument, + }, + }, + { + name: "invalid roles, error", + fields: fields{ + eventstore: eventstoreExpect( + t, + ), + }, + args: args{ + ctx: context.Background(), + member: &domain.Member{ + ObjectRoot: models.ObjectRoot{ + AggregateID: "project1", + }, + UserID: "user1", + Roles: []string{"PROJECT_OWNER"}, + }, + resourceOwner: "org1", + }, + res: res{ + err: caos_errs.IsErrorInvalidArgument, + }, + }, + { + name: "member not existing, not found error", + fields: fields{ + eventstore: eventstoreExpect( + t, + expectFilter(), + ), + zitadelRoles: []authz.RoleMapping{ + { + Role: domain.RoleProjectOwner, + }, + }, + }, + args: args{ + ctx: context.Background(), + member: &domain.Member{ + ObjectRoot: models.ObjectRoot{ + AggregateID: "project1", + }, + UserID: "user1", + Roles: []string{"PROJECT_OWNER"}, + }, + resourceOwner: "org1", + }, + res: res{ + err: caos_errs.IsNotFound, + }, + }, + { + name: "member not changed, precondition error", + fields: fields{ + eventstore: eventstoreExpect( + t, + expectFilter( + eventFromEventPusher( + project.NewProjectMemberAddedEvent(context.Background(), + &project.NewAggregate("project1", "org1").Aggregate, + "user1", + []string{"PROJECT_OWNER"}..., + ), + ), + ), + ), + zitadelRoles: []authz.RoleMapping{ + { + Role: domain.RoleProjectOwner, + }, + }, + }, + args: args{ + ctx: context.Background(), + member: &domain.Member{ + ObjectRoot: models.ObjectRoot{ + AggregateID: "project1", + }, + UserID: "user1", + Roles: []string{"PROJECT_OWNER"}, + }, + resourceOwner: "org1", + }, + res: res{ + err: caos_errs.IsPreconditionFailed, + }, + }, + { + name: "member change, ok", + fields: fields{ + eventstore: eventstoreExpect( + t, + expectFilter( + eventFromEventPusher( + project.NewProjectMemberAddedEvent(context.Background(), + &project.NewAggregate("project1", "org1").Aggregate, + "user1", + []string{"PROJECT_OWNER"}..., + ), + ), + ), + expectPush( + []*repository.Event{ + eventFromEventPusher(project.NewProjectMemberChangedEvent(context.Background(), + &project.NewAggregate("project1", "org1").Aggregate, + "user1", + []string{"PROJECT_OWNER", "PROJECT_VIEWER"}..., + )), + }, + ), + ), + zitadelRoles: []authz.RoleMapping{ + { + Role: domain.RoleProjectOwner, + }, + { + Role: "PROJECT_VIEWER", + }, + }, + }, + args: args{ + ctx: context.Background(), + member: &domain.Member{ + ObjectRoot: models.ObjectRoot{ + AggregateID: "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"}, + }, + }, + }, + } + 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.ChangeProjectMember(tt.args.ctx, tt.args.member, tt.args.resourceOwner) + 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_RemoveProjectMember(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: caos_errs.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: caos_errs.IsErrorInvalidArgument, + }, + }, + { + name: "member not existing, nil result", + fields: fields{ + eventstore: eventstoreExpect( + t, + expectFilter(), + ), + }, + args: args{ + ctx: context.Background(), + projectID: "project1", + userID: "user1", + resourceOwner: "org1", + }, + res: res{ + want: nil, + }, + }, + { + 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( + []*repository.Event{ + eventFromEventPusher(project.NewProjectMemberRemovedEvent(context.Background(), + &project.NewAggregate("project1", "org1").Aggregate, + "user1", + )), + }, + uniqueConstraintsFromEventConstraint(member.NewRemoveMemberUniqueConstraint("project1", "user1")), + ), + ), + }, + args: args{ + ctx: context.Background(), + projectID: "project1", + userID: "user1", + resourceOwner: "org1", + }, + res: res{ + want: &domain.ObjectDetails{ + ResourceOwner: "org1", + }, + }, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + r := &Commands{ + eventstore: tt.fields.eventstore, + } + got, err := r.RemoveProjectMember(tt.args.ctx, tt.args.projectID, tt.args.userID, tt.args.resourceOwner) + 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) + } + }) + } +} diff --git a/internal/command/project_role.go b/internal/command/project_role.go index d80b074cc..70baf3192 100644 --- a/internal/command/project_role.go +++ b/internal/command/project_role.go @@ -17,7 +17,7 @@ func (c *Commands) AddProjectRole(ctx context.Context, projectRole *domain.Proje roleWriteModel := NewProjectRoleWriteModelWithKey(projectRole.Key, projectRole.AggregateID, resourceOwner) projectAgg := ProjectAggregateFromWriteModel(&roleWriteModel.WriteModel) - events, err := c.addProjectRoles(ctx, projectAgg, projectRole.AggregateID, projectRole) + events, err := c.addProjectRoles(ctx, projectAgg, projectRole) if err != nil { return nil, err } @@ -40,7 +40,7 @@ func (c *Commands) BulkAddProjectRole(ctx context.Context, projectID, resourceOw roleWriteModel := NewProjectRoleWriteModel(projectID, resourceOwner) projectAgg := ProjectAggregateFromWriteModel(&roleWriteModel.WriteModel) - events, err := c.addProjectRoles(ctx, projectAgg, projectID, projectRoles...) + events, err := c.addProjectRoles(ctx, projectAgg, projectRoles...) if err != nil { return details, err } @@ -56,11 +56,12 @@ func (c *Commands) BulkAddProjectRole(ctx context.Context, projectID, resourceOw return writeModelToObjectDetails(&roleWriteModel.WriteModel), nil } -func (c *Commands) addProjectRoles(ctx context.Context, projectAgg *eventstore.Aggregate, projectID string, projectRoles ...*domain.ProjectRole) ([]eventstore.EventPusher, error) { +func (c *Commands) addProjectRoles(ctx context.Context, projectAgg *eventstore.Aggregate, projectRoles ...*domain.ProjectRole) ([]eventstore.EventPusher, error) { var events []eventstore.EventPusher for _, projectRole := range projectRoles { + projectRole.AggregateID = projectAgg.ID if !projectRole.IsValid() { - return nil, caos_errs.ThrowPreconditionFailed(nil, "COMMAND-4m9vS", "Errors.Project.Invalid") + return nil, caos_errs.ThrowInvalidArgument(nil, "COMMAND-4m9vS", "Errors.Project.Role.Invalid") } events = append(events, project.NewRoleAddedEvent( ctx, @@ -68,7 +69,6 @@ func (c *Commands) addProjectRoles(ctx context.Context, projectAgg *eventstore.A projectRole.Key, projectRole.DisplayName, projectRole.Group, - projectID, )) } @@ -77,7 +77,7 @@ func (c *Commands) addProjectRoles(ctx context.Context, projectAgg *eventstore.A func (c *Commands) ChangeProjectRole(ctx context.Context, projectRole *domain.ProjectRole, resourceOwner string) (_ *domain.ProjectRole, err error) { if !projectRole.IsValid() { - return nil, caos_errs.ThrowPreconditionFailed(nil, "COMMAND-4m9vS", "Errors.Project.Invalid") + return nil, caos_errs.ThrowInvalidArgument(nil, "COMMAND-4m9vS", "Errors.Project.Invalid") } err = c.checkProjectExists(ctx, projectRole.AggregateID, resourceOwner) if err != nil { @@ -115,7 +115,7 @@ func (c *Commands) ChangeProjectRole(ctx context.Context, projectRole *domain.Pr func (c *Commands) RemoveProjectRole(ctx context.Context, projectID, key, resourceOwner string, cascadingProjectGrantIds []string, cascadeUserGrantIDs ...string) (details *domain.ObjectDetails, err error) { if projectID == "" || key == "" { - return details, caos_errs.ThrowPreconditionFailed(nil, "COMMAND-4m9vS", "Errors.Project.Role.Invalid") + return details, caos_errs.ThrowInvalidArgument(nil, "COMMAND-4m9vS", "Errors.Project.Role.Invalid") } existingRole, err := c.getProjectRoleWriteModelByID(ctx, key, projectID, resourceOwner) if err != nil { @@ -126,7 +126,7 @@ func (c *Commands) RemoveProjectRole(ctx context.Context, projectID, key, resour } projectAgg := ProjectAggregateFromWriteModel(&existingRole.WriteModel) events := []eventstore.EventPusher{ - project.NewRoleRemovedEvent(ctx, projectAgg, key, projectID), + project.NewRoleRemovedEvent(ctx, projectAgg, key), } for _, projectGrantID := range cascadingProjectGrantIds { diff --git a/internal/command/project_role_model.go b/internal/command/project_role_model.go index ee49de695..021b67e56 100644 --- a/internal/command/project_role_model.go +++ b/internal/command/project_role_model.go @@ -101,7 +101,6 @@ func (wm *ProjectRoleWriteModel) NewProjectRoleChangedEvent( ) (*project.RoleChangedEvent, bool, error) { changes := make([]project.RoleChanges, 0) var err error - changes = append(changes, project.ChangeKey(key)) if wm.DisplayName != displayName { changes = append(changes, project.ChangeDisplayName(displayName)) @@ -112,7 +111,7 @@ func (wm *ProjectRoleWriteModel) NewProjectRoleChangedEvent( if len(changes) == 0 { return nil, false, nil } - changeEvent, err := project.NewRoleChangedEvent(ctx, aggregate, changes) + changeEvent, err := project.NewRoleChangedEvent(ctx, aggregate, key, changes) if err != nil { return nil, false, err } diff --git a/internal/command/project_role_test.go b/internal/command/project_role_test.go new file mode 100644 index 000000000..a4bebdd1e --- /dev/null +++ b/internal/command/project_role_test.go @@ -0,0 +1,1053 @@ +package command + +import ( + "context" + "github.com/caos/zitadel/internal/domain" + caos_errs "github.com/caos/zitadel/internal/errors" + "github.com/caos/zitadel/internal/eventstore" + "github.com/caos/zitadel/internal/eventstore/repository" + "github.com/caos/zitadel/internal/eventstore/v1/models" + "github.com/caos/zitadel/internal/repository/project" + "github.com/caos/zitadel/internal/repository/usergrant" + "github.com/stretchr/testify/assert" + "testing" +) + +func TestCommandSide_AddProjectRole(t *testing.T) { + type fields struct { + eventstore *eventstore.Eventstore + } + type args struct { + ctx context.Context + role *domain.ProjectRole + resourceOwner string + } + type res struct { + want *domain.ProjectRole + err func(error) bool + } + tests := []struct { + name string + fields fields + args args + res res + }{ + { + name: "project not existing, precondition error", + fields: fields{ + eventstore: eventstoreExpect( + t, + expectFilter( + eventFromEventPusher( + project.NewProjectAddedEvent(context.Background(), + &project.NewAggregate("project1", "org1").Aggregate, + "projectname1", + ), + ), + eventFromEventPusher( + project.NewProjectRemovedEvent(context.Background(), + &project.NewAggregate("project1", "org1").Aggregate, + "projectname1", + ), + ), + ), + ), + }, + args: args{ + ctx: context.Background(), + role: &domain.ProjectRole{ + ObjectRoot: models.ObjectRoot{ + AggregateID: "project1", + }, + Key: "key1", + }, + resourceOwner: "org1", + }, + res: res{ + err: caos_errs.IsPreconditionFailed, + }, + }, + { + name: "invalid role, error", + fields: fields{ + eventstore: eventstoreExpect( + t, + expectFilter( + eventFromEventPusher( + project.NewProjectAddedEvent(context.Background(), + &project.NewAggregate("project1", "org1").Aggregate, + "projectname1", + ), + ), + ), + ), + }, + args: args{ + ctx: context.Background(), + role: &domain.ProjectRole{ + ObjectRoot: models.ObjectRoot{ + AggregateID: "project1", + }, + }, + resourceOwner: "org1", + }, + res: res{ + err: caos_errs.IsErrorInvalidArgument, + }, + }, + { + name: "role key already exists, already exists error", + fields: fields{ + eventstore: eventstoreExpect( + t, + expectFilter( + eventFromEventPusher( + project.NewProjectAddedEvent(context.Background(), + &project.NewAggregate("project1", "org1").Aggregate, + "projectname1", + ), + ), + ), + expectPushFailed(caos_errs.ThrowAlreadyExists(nil, "id", "internal"), + []*repository.Event{ + eventFromEventPusher(project.NewRoleAddedEvent( + context.Background(), + &project.NewAggregate("project1", "org1").Aggregate, + "key1", + "key", + "group", + ), + ), + }, + uniqueConstraintsFromEventConstraint(project.NewAddProjectRoleUniqueConstraint("key1", "project1")), + ), + ), + }, + args: args{ + ctx: context.Background(), + role: &domain.ProjectRole{ + ObjectRoot: models.ObjectRoot{ + AggregateID: "project1", + }, + Key: "key1", + DisplayName: "key", + Group: "group", + }, + resourceOwner: "org1", + }, + res: res{ + err: caos_errs.IsErrorAlreadyExists, + }, + }, + { + name: "add role,ok", + fields: fields{ + eventstore: eventstoreExpect( + t, + expectFilter( + eventFromEventPusher( + project.NewProjectAddedEvent(context.Background(), + &project.NewAggregate("project1", "org1").Aggregate, + "projectname1", + ), + ), + ), + expectPush( + []*repository.Event{ + eventFromEventPusher(project.NewRoleAddedEvent( + context.Background(), + &project.NewAggregate("project1", "org1").Aggregate, + "key1", + "key", + "group", + ), + ), + }, + uniqueConstraintsFromEventConstraint(project.NewAddProjectRoleUniqueConstraint("key1", "project1")), + ), + ), + }, + args: args{ + ctx: context.Background(), + role: &domain.ProjectRole{ + ObjectRoot: models.ObjectRoot{ + AggregateID: "project1", + }, + Key: "key1", + DisplayName: "key", + Group: "group", + }, + resourceOwner: "org1", + }, + res: res{ + want: &domain.ProjectRole{ + ObjectRoot: models.ObjectRoot{ + AggregateID: "project1", + ResourceOwner: "org1", + }, + Key: "key1", + DisplayName: "key", + Group: "group", + }, + }, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + r := &Commands{ + eventstore: tt.fields.eventstore, + } + got, err := r.AddProjectRole(tt.args.ctx, tt.args.role, tt.args.resourceOwner) + 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_BulkAddProjectRole(t *testing.T) { + type fields struct { + eventstore *eventstore.Eventstore + } + type args struct { + ctx context.Context + roles []*domain.ProjectRole + projectID string + resourceOwner string + } + type res struct { + want *domain.ObjectDetails + err func(error) bool + } + tests := []struct { + name string + fields fields + args args + res res + }{ + { + name: "project not existing, precondition error", + fields: fields{ + eventstore: eventstoreExpect( + t, + expectFilter( + eventFromEventPusher( + project.NewProjectAddedEvent(context.Background(), + &project.NewAggregate("project1", "org1").Aggregate, + "projectname1", + ), + ), + eventFromEventPusher( + project.NewProjectRemovedEvent(context.Background(), + &project.NewAggregate("project1", "org1").Aggregate, + "projectname1", + ), + ), + ), + ), + }, + args: args{ + ctx: context.Background(), + roles: []*domain.ProjectRole{ + { + ObjectRoot: models.ObjectRoot{ + AggregateID: "project1", + }, + Key: "key1", + }, + }, + resourceOwner: "org1", + }, + res: res{ + err: caos_errs.IsPreconditionFailed, + }, + }, + { + name: "invalid role, error", + fields: fields{ + eventstore: eventstoreExpect( + t, + expectFilter( + eventFromEventPusher( + project.NewProjectAddedEvent(context.Background(), + &project.NewAggregate("project1", "org1").Aggregate, + "projectname1", + ), + ), + ), + ), + }, + args: args{ + ctx: context.Background(), + roles: []*domain.ProjectRole{ + { + ObjectRoot: models.ObjectRoot{}, + }, + { + ObjectRoot: models.ObjectRoot{}, + }, + }, + projectID: "project1", + resourceOwner: "org1", + }, + res: res{ + err: caos_errs.IsErrorInvalidArgument, + }, + }, + { + name: "role key already exists, already exists error", + fields: fields{ + eventstore: eventstoreExpect( + t, + expectFilter( + eventFromEventPusher( + project.NewProjectAddedEvent(context.Background(), + &project.NewAggregate("project1", "org1").Aggregate, + "projectname1", + ), + ), + ), + expectPushFailed(caos_errs.ThrowAlreadyExists(nil, "id", "internal"), + []*repository.Event{ + eventFromEventPusher(project.NewRoleAddedEvent( + context.Background(), + &project.NewAggregate("project1", "org1").Aggregate, + "key1", + "key", + "group", + ), + ), + eventFromEventPusher(project.NewRoleAddedEvent( + context.Background(), + &project.NewAggregate("project1", "org1").Aggregate, + "key2", + "key2", + "group", + ), + ), + }, + uniqueConstraintsFromEventConstraint(project.NewAddProjectRoleUniqueConstraint("key1", "project1")), + uniqueConstraintsFromEventConstraint(project.NewAddProjectRoleUniqueConstraint("key2", "project1")), + ), + ), + }, + args: args{ + ctx: context.Background(), + roles: []*domain.ProjectRole{ + { + Key: "key1", + DisplayName: "key", + Group: "group", + }, + { + Key: "key2", + DisplayName: "key2", + Group: "group", + }, + }, + projectID: "project1", + resourceOwner: "org1", + }, + res: res{ + err: caos_errs.IsErrorAlreadyExists, + }, + }, + { + name: "add roles,ok", + fields: fields{ + eventstore: eventstoreExpect( + t, + expectFilter( + eventFromEventPusher( + project.NewProjectAddedEvent(context.Background(), + &project.NewAggregate("project1", "org1").Aggregate, + "projectname1", + ), + ), + ), + expectPush( + []*repository.Event{ + eventFromEventPusher(project.NewRoleAddedEvent( + context.Background(), + &project.NewAggregate("project1", "org1").Aggregate, + "key1", + "key", + "group", + ), + ), + eventFromEventPusher(project.NewRoleAddedEvent( + context.Background(), + &project.NewAggregate("project1", "org1").Aggregate, + "key2", + "key2", + "group", + ), + ), + }, + uniqueConstraintsFromEventConstraint(project.NewAddProjectRoleUniqueConstraint("key1", "project1")), + uniqueConstraintsFromEventConstraint(project.NewAddProjectRoleUniqueConstraint("key2", "project1")), + ), + ), + }, + args: args{ + ctx: context.Background(), + roles: []*domain.ProjectRole{ + { + Key: "key1", + DisplayName: "key", + Group: "group", + }, + { + Key: "key2", + DisplayName: "key2", + Group: "group", + }, + }, + projectID: "project1", + resourceOwner: "org1", + }, + res: res{ + want: &domain.ObjectDetails{ + ResourceOwner: "org1", + }, + }, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + r := &Commands{ + eventstore: tt.fields.eventstore, + } + got, err := r.BulkAddProjectRole(tt.args.ctx, tt.args.projectID, tt.args.resourceOwner, tt.args.roles) + 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_ChangeProjectRole(t *testing.T) { + type fields struct { + eventstore *eventstore.Eventstore + } + type args struct { + ctx context.Context + role *domain.ProjectRole + resourceOwner string + } + type res struct { + want *domain.ProjectRole + err func(error) bool + } + tests := []struct { + name string + fields fields + args args + res res + }{ + { + name: "invalid role, error", + fields: fields{ + eventstore: eventstoreExpect( + t, + ), + }, + args: args{ + ctx: context.Background(), + role: &domain.ProjectRole{ + ObjectRoot: models.ObjectRoot{ + AggregateID: "project1", + }, + }, + resourceOwner: "org1", + }, + res: res{ + err: caos_errs.IsErrorInvalidArgument, + }, + }, + { + name: "project not existing, precondition error", + fields: fields{ + eventstore: eventstoreExpect( + t, + expectFilter( + eventFromEventPusher( + project.NewProjectAddedEvent(context.Background(), + &project.NewAggregate("project1", "org1").Aggregate, + "projectname1", + ), + ), + eventFromEventPusher( + project.NewProjectRemovedEvent(context.Background(), + &project.NewAggregate("project1", "org1").Aggregate, + "projectname1", + ), + ), + ), + ), + }, + args: args{ + ctx: context.Background(), + role: &domain.ProjectRole{ + ObjectRoot: models.ObjectRoot{ + AggregateID: "project1", + }, + Key: "key1", + }, + resourceOwner: "org1", + }, + res: res{ + err: caos_errs.IsPreconditionFailed, + }, + }, + { + name: "role removed, not found error", + fields: fields{ + eventstore: eventstoreExpect( + t, + expectFilter( + eventFromEventPusher( + project.NewProjectAddedEvent(context.Background(), + &project.NewAggregate("project1", "org1").Aggregate, + "projectname1", + ), + ), + ), + expectFilter( + eventFromEventPusher( + project.NewRoleAddedEvent(context.Background(), + &project.NewAggregate("project1", "org1").Aggregate, + "key1", + "key", + "group", + ), + ), + eventFromEventPusher( + project.NewRoleRemovedEvent(context.Background(), + &project.NewAggregate("project1", "org1").Aggregate, + "key1", + ), + ), + ), + ), + }, + args: args{ + ctx: context.Background(), + role: &domain.ProjectRole{ + ObjectRoot: models.ObjectRoot{ + AggregateID: "project1", + }, + Key: "key1", + DisplayName: "key", + Group: "group", + }, + resourceOwner: "org1", + }, + res: res{ + err: caos_errs.IsNotFound, + }, + }, + { + name: "role not changed, precondition error", + fields: fields{ + eventstore: eventstoreExpect( + t, + expectFilter( + eventFromEventPusher( + project.NewProjectAddedEvent(context.Background(), + &project.NewAggregate("project1", "org1").Aggregate, + "projectname1", + ), + ), + ), + expectFilter( + eventFromEventPusher( + project.NewRoleAddedEvent(context.Background(), + &project.NewAggregate("project1", "org1").Aggregate, + "key1", + "key", + "group", + ), + ), + ), + ), + }, + args: args{ + ctx: context.Background(), + role: &domain.ProjectRole{ + ObjectRoot: models.ObjectRoot{ + AggregateID: "project1", + }, + Key: "key1", + DisplayName: "key", + Group: "group", + }, + resourceOwner: "org1", + }, + res: res{ + err: caos_errs.IsPreconditionFailed, + }, + }, + { + name: "role changed, ok", + fields: fields{ + eventstore: eventstoreExpect( + t, + expectFilter( + eventFromEventPusher( + project.NewProjectAddedEvent(context.Background(), + &project.NewAggregate("project1", "org1").Aggregate, + "projectname1", + ), + ), + ), + expectFilter( + eventFromEventPusher( + project.NewRoleAddedEvent(context.Background(), + &project.NewAggregate("project1", "org1").Aggregate, + "key1", + "key", + "group", + ), + ), + ), + expectPush( + []*repository.Event{ + eventFromEventPusher( + newRoleChangedEvent(context.Background(), "project1", "org1", "key1", "keychanged", "groupchanged"), + ), + }, + ), + ), + }, + args: args{ + ctx: context.Background(), + role: &domain.ProjectRole{ + ObjectRoot: models.ObjectRoot{ + AggregateID: "project1", + }, + Key: "key1", + DisplayName: "keychanged", + Group: "groupchanged", + }, + resourceOwner: "org1", + }, + res: res{ + want: &domain.ProjectRole{ + ObjectRoot: models.ObjectRoot{ + AggregateID: "project1", + ResourceOwner: "org1", + }, + Key: "key1", + DisplayName: "keychanged", + Group: "groupchanged", + }, + }, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + r := &Commands{ + eventstore: tt.fields.eventstore, + } + got, err := r.ChangeProjectRole(tt.args.ctx, tt.args.role, tt.args.resourceOwner) + 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_RemoveProjectRole(t *testing.T) { + type fields struct { + eventstore *eventstore.Eventstore + } + type args struct { + ctx context.Context + projectID string + key string + resourceOwner string + cascadingProjectGrantIDs []string + cascadingUserGrantIDs []string + } + type res struct { + want *domain.ObjectDetails + err func(error) bool + } + tests := []struct { + name string + fields fields + args args + res res + }{ + { + name: "invalid projectid, error", + fields: fields{ + eventstore: eventstoreExpect( + t, + ), + }, + args: args{ + ctx: context.Background(), + key: "key1", + resourceOwner: "org1", + }, + res: res{ + err: caos_errs.IsErrorInvalidArgument, + }, + }, + { + name: "invalid key, error", + fields: fields{ + eventstore: eventstoreExpect( + t, + ), + }, + args: args{ + ctx: context.Background(), + key: "", + projectID: "project1", + resourceOwner: "org1", + }, + res: res{ + err: caos_errs.IsErrorInvalidArgument, + }, + }, + { + name: "role not existing, not found error", + fields: fields{ + eventstore: eventstoreExpect( + t, + expectFilter(), + ), + }, + args: args{ + ctx: context.Background(), + projectID: "project1", + key: "key", + resourceOwner: "org1", + }, + res: res{ + err: caos_errs.IsNotFound, + }, + }, + { + name: "role removed, not found error", + fields: fields{ + eventstore: eventstoreExpect( + t, + expectFilter( + eventFromEventPusher( + project.NewRoleAddedEvent(context.Background(), + &project.NewAggregate("project1", "org1").Aggregate, + "key1", + "key", + "ggroup", + ), + ), + eventFromEventPusher( + project.NewRoleRemovedEvent(context.Background(), + &project.NewAggregate("project1", "org1").Aggregate, + "key1", + ), + ), + ), + ), + }, + args: args{ + ctx: context.Background(), + projectID: "project1", + key: "key", + resourceOwner: "org1", + }, + res: res{ + err: caos_errs.IsNotFound, + }, + }, + { + name: "role removed, ok", + fields: fields{ + eventstore: eventstoreExpect( + t, + expectFilter( + eventFromEventPusher( + project.NewRoleAddedEvent(context.Background(), + &project.NewAggregate("project1", "org1").Aggregate, + "key1", + "key", + "group", + ), + ), + ), + expectPush( + []*repository.Event{ + eventFromEventPusher( + project.NewRoleRemovedEvent(context.Background(), + &project.NewAggregate("project1", "org1").Aggregate, + "key1", + ), + ), + }, + uniqueConstraintsFromEventConstraint(project.NewRemoveProjectRoleUniqueConstraint("key1", "project1")), + ), + ), + }, + args: args{ + ctx: context.Background(), + projectID: "project1", + key: "key1", + resourceOwner: "org1", + }, + res: res{ + want: &domain.ObjectDetails{ + ResourceOwner: "org1", + }, + }, + }, + { + name: "role removed with cascadingProjectGrantids, grant not found, ok", + fields: fields{ + eventstore: eventstoreExpect( + t, + expectFilter( + eventFromEventPusher( + project.NewRoleAddedEvent(context.Background(), + &project.NewAggregate("project1", "org1").Aggregate, + "key1", + "key", + "group", + ), + ), + ), + expectFilter(), + expectPush( + []*repository.Event{ + eventFromEventPusher( + project.NewRoleRemovedEvent(context.Background(), + &project.NewAggregate("project1", "org1").Aggregate, + "key1", + ), + ), + }, + uniqueConstraintsFromEventConstraint(project.NewRemoveProjectRoleUniqueConstraint("key1", "project1")), + ), + ), + }, + args: args{ + ctx: context.Background(), + projectID: "project1", + key: "key1", + resourceOwner: "org1", + cascadingProjectGrantIDs: []string{"projectgrant1"}, + }, + res: res{ + want: &domain.ObjectDetails{ + ResourceOwner: "org1", + }, + }, + }, + { + name: "role removed with cascadingProjectGrantids, ok", + fields: fields{ + eventstore: eventstoreExpect( + t, + expectFilter( + eventFromEventPusher( + project.NewRoleAddedEvent(context.Background(), + &project.NewAggregate("project1", "org1").Aggregate, + "key1", + "key", + "group", + ), + ), + ), + expectFilter( + eventFromEventPusher( + project.NewGrantAddedEvent(context.Background(), + &project.NewAggregate("project1", "org1").Aggregate, + "projectgrant1", + "org2", + []string{"key1"}, + ), + ), + ), + expectPush( + []*repository.Event{ + eventFromEventPusher( + project.NewRoleRemovedEvent(context.Background(), + &project.NewAggregate("project1", "org1").Aggregate, + "key1", + ), + ), + eventFromEventPusher( + project.NewGrantCascadeChangedEvent(context.Background(), + &project.NewAggregate("project1", "org1").Aggregate, + "projectgrant1", + []string{}, + ), + ), + }, + uniqueConstraintsFromEventConstraint(project.NewRemoveProjectRoleUniqueConstraint("key1", "project1")), + ), + ), + }, + args: args{ + ctx: context.Background(), + projectID: "project1", + key: "key1", + resourceOwner: "org1", + cascadingProjectGrantIDs: []string{"projectgrant1"}, + }, + res: res{ + want: &domain.ObjectDetails{ + ResourceOwner: "org1", + }, + }, + }, + { + name: "role removed with cascadingUserGrantIDs, grant not found, ok", + fields: fields{ + eventstore: eventstoreExpect( + t, + expectFilter( + eventFromEventPusher( + project.NewRoleAddedEvent(context.Background(), + &project.NewAggregate("project1", "org1").Aggregate, + "key1", + "key", + "group", + ), + ), + ), + expectFilter(), + expectPush( + []*repository.Event{ + eventFromEventPusher( + project.NewRoleRemovedEvent(context.Background(), + &project.NewAggregate("project1", "org1").Aggregate, + "key1", + ), + ), + }, + uniqueConstraintsFromEventConstraint(project.NewRemoveProjectRoleUniqueConstraint("key1", "project1")), + ), + ), + }, + args: args{ + ctx: context.Background(), + projectID: "project1", + key: "key1", + resourceOwner: "org1", + cascadingUserGrantIDs: []string{"usergrant1"}, + }, + res: res{ + want: &domain.ObjectDetails{ + ResourceOwner: "org1", + }, + }, + }, + { + name: "role removed with cascadingUserGrantIDs, ok", + fields: fields{ + eventstore: eventstoreExpect( + t, + expectFilter( + eventFromEventPusher( + project.NewRoleAddedEvent(context.Background(), + &project.NewAggregate("project1", "org1").Aggregate, + "key1", + "key", + "group", + ), + ), + ), + expectFilter( + eventFromEventPusher( + usergrant.NewUserGrantAddedEvent(context.Background(), + &usergrant.NewAggregate("usergrant1", "org1").Aggregate, + "user1", + "project1", + "", + []string{"key1"})), + ), + expectPush( + []*repository.Event{ + eventFromEventPusher( + project.NewRoleRemovedEvent(context.Background(), + &project.NewAggregate("project1", "org1").Aggregate, + "key1", + ), + ), + eventFromEventPusher( + usergrant.NewUserGrantCascadeChangedEvent(context.Background(), + &usergrant.NewAggregate("usergrant1", "org1").Aggregate, + []string{}, + ), + ), + }, + uniqueConstraintsFromEventConstraint(project.NewRemoveProjectRoleUniqueConstraint("key1", "project1")), + ), + ), + }, + args: args{ + ctx: context.Background(), + projectID: "project1", + key: "key1", + resourceOwner: "org1", + cascadingUserGrantIDs: []string{"usergrant1"}, + }, + res: res{ + want: &domain.ObjectDetails{ + ResourceOwner: "org1", + }, + }, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + r := &Commands{ + eventstore: tt.fields.eventstore, + } + got, err := r.RemoveProjectRole(tt.args.ctx, tt.args.projectID, tt.args.key, tt.args.resourceOwner, tt.args.cascadingProjectGrantIDs, tt.args.cascadingUserGrantIDs...) + 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 newRoleChangedEvent(ctx context.Context, projectID, resourceOwner, key, displayName, group string) *project.RoleChangedEvent { + event, _ := project.NewRoleChangedEvent(ctx, + &project.NewAggregate(projectID, resourceOwner).Aggregate, + key, + []project.RoleChanges{ + project.ChangeDisplayName(displayName), + project.ChangeGroup(group), + }, + ) + return event +} diff --git a/internal/command/project_test.go b/internal/command/project_test.go new file mode 100644 index 000000000..bfb1afeb5 --- /dev/null +++ b/internal/command/project_test.go @@ -0,0 +1,1011 @@ +package command + +import ( + "context" + "github.com/caos/zitadel/internal/domain" + caos_errs "github.com/caos/zitadel/internal/errors" + "github.com/caos/zitadel/internal/eventstore" + "github.com/caos/zitadel/internal/eventstore/repository" + "github.com/caos/zitadel/internal/eventstore/v1/models" + "github.com/caos/zitadel/internal/id" + id_mock "github.com/caos/zitadel/internal/id/mock" + "github.com/caos/zitadel/internal/repository/iam" + "github.com/caos/zitadel/internal/repository/member" + "github.com/caos/zitadel/internal/repository/project" + "github.com/stretchr/testify/assert" + "testing" +) + +func TestCommandSide_AddProject(t *testing.T) { + type fields struct { + eventstore *eventstore.Eventstore + idGenerator id.Generator + } + type args struct { + ctx context.Context + project *domain.Project + resourceOwner string + ownerID string + } + type res struct { + want *domain.Project + err func(error) bool + } + tests := []struct { + name string + fields fields + args args + res res + }{ + { + name: "invalid project, error", + fields: fields{ + eventstore: eventstoreExpect( + t, + ), + }, + args: args{ + ctx: context.Background(), + project: &domain.Project{}, + resourceOwner: "org1", + }, + res: res{ + err: caos_errs.IsErrorInvalidArgument, + }, + }, + { + name: "org with project owner, error already exists", + fields: fields{ + eventstore: eventstoreExpect( + t, + expectFilter( + eventFromEventPusher( + iam.NewGlobalOrgSetEventEvent(context.Background(), + &iam.NewAggregate().Aggregate, + "globalorg", + ), + ), + ), + expectPushFailed(caos_errs.ThrowAlreadyExists(nil, "ERROR", "internl"), + []*repository.Event{ + eventFromEventPusher(project.NewProjectAddedEvent( + context.Background(), + &project.NewAggregate("project1", "org1").Aggregate, + "project", + ), + ), + eventFromEventPusher(project.NewProjectMemberAddedEvent( + context.Background(), + &project.NewAggregate("project1", "org1").Aggregate, + "user1", + []string{domain.RoleProjectOwner}..., + ), + ), + }, + uniqueConstraintsFromEventConstraint(project.NewAddProjectNameUniqueConstraint("project", "org1")), + uniqueConstraintsFromEventConstraint(member.NewAddMemberUniqueConstraint("project1", "user1")), + ), + ), + idGenerator: id_mock.NewIDGeneratorExpectIDs(t, "project1"), + }, + args: args{ + ctx: context.Background(), + project: &domain.Project{ + Name: "project", + }, + resourceOwner: "org1", + ownerID: "user1", + }, + res: res{ + err: caos_errs.IsErrorAlreadyExists, + }, + }, + { + name: "global org with project owner global, ok", + fields: fields{ + eventstore: eventstoreExpect( + t, + expectFilter( + eventFromEventPusher( + iam.NewGlobalOrgSetEventEvent(context.Background(), + &iam.NewAggregate().Aggregate, + "globalorg", + ), + ), + ), + expectPush( + []*repository.Event{ + eventFromEventPusher(project.NewProjectAddedEvent( + context.Background(), + &project.NewAggregate("project1", "globalorg").Aggregate, + "project", + ), + ), + eventFromEventPusher(project.NewProjectMemberAddedEvent( + context.Background(), + &project.NewAggregate("project1", "globalorg").Aggregate, + "user1", + []string{domain.RoleProjectOwnerGlobal}..., + ), + ), + }, + uniqueConstraintsFromEventConstraint(project.NewAddProjectNameUniqueConstraint("project", "globalorg")), + uniqueConstraintsFromEventConstraint(member.NewAddMemberUniqueConstraint("project1", "user1")), + ), + ), + idGenerator: id_mock.NewIDGeneratorExpectIDs(t, "project1"), + }, + args: args{ + ctx: context.Background(), + project: &domain.Project{ + Name: "project", + }, + resourceOwner: "globalorg", + ownerID: "user1", + }, + res: res{ + want: &domain.Project{ + ObjectRoot: models.ObjectRoot{ + ResourceOwner: "globalorg", + AggregateID: "project1", + }, + Name: "project", + }, + }, + }, + { + name: "org with project owner, ok", + fields: fields{ + eventstore: eventstoreExpect( + t, + expectFilter( + eventFromEventPusher( + iam.NewGlobalOrgSetEventEvent(context.Background(), + &iam.NewAggregate().Aggregate, + "globalorg", + ), + ), + ), + expectPush( + []*repository.Event{ + eventFromEventPusher(project.NewProjectAddedEvent( + context.Background(), + &project.NewAggregate("project1", "org1").Aggregate, + "project", + ), + ), + eventFromEventPusher(project.NewProjectMemberAddedEvent( + context.Background(), + &project.NewAggregate("project1", "org1").Aggregate, + "user1", + []string{domain.RoleProjectOwner}..., + ), + ), + }, + uniqueConstraintsFromEventConstraint(project.NewAddProjectNameUniqueConstraint("project", "org1")), + uniqueConstraintsFromEventConstraint(member.NewAddMemberUniqueConstraint("project1", "user1")), + ), + ), + idGenerator: id_mock.NewIDGeneratorExpectIDs(t, "project1"), + }, + args: args{ + ctx: context.Background(), + project: &domain.Project{ + Name: "project", + }, + resourceOwner: "org1", + ownerID: "user1", + }, + res: res{ + want: &domain.Project{ + ObjectRoot: models.ObjectRoot{ + ResourceOwner: "org1", + AggregateID: "project1", + }, + Name: "project", + }, + }, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + r := &Commands{ + eventstore: tt.fields.eventstore, + idGenerator: tt.fields.idGenerator, + } + got, err := r.AddProject(tt.args.ctx, tt.args.project, tt.args.resourceOwner, tt.args.ownerID) + 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_ChangeProject(t *testing.T) { + type fields struct { + eventstore *eventstore.Eventstore + } + type args struct { + ctx context.Context + project *domain.Project + resourceOwner string + } + type res struct { + want *domain.Project + err func(error) bool + } + tests := []struct { + name string + fields fields + args args + res res + }{ + { + name: "invalid project, invalid error", + fields: fields{ + eventstore: eventstoreExpect( + t, + ), + }, + args: args{ + ctx: context.Background(), + project: &domain.Project{ + ObjectRoot: models.ObjectRoot{ + AggregateID: "project1", + }, + }, + resourceOwner: "org1", + }, + res: res{ + err: caos_errs.IsErrorInvalidArgument, + }, + }, + { + name: "invalid project empty aggregateid, invalid error", + fields: fields{ + eventstore: eventstoreExpect( + t, + ), + }, + args: args{ + ctx: context.Background(), + project: &domain.Project{ + Name: "project", + }, + resourceOwner: "org1", + }, + res: res{ + err: caos_errs.IsErrorInvalidArgument, + }, + }, + { + name: "project not existing, not found error", + fields: fields{ + eventstore: eventstoreExpect( + t, + expectFilter(), + ), + }, + args: args{ + ctx: context.Background(), + project: &domain.Project{ + ObjectRoot: models.ObjectRoot{ + AggregateID: "project1", + }, + Name: "project change", + }, + resourceOwner: "org1", + }, + res: res{ + err: caos_errs.IsNotFound, + }, + }, + { + name: "project removed, not found error", + fields: fields{ + eventstore: eventstoreExpect( + t, + expectFilter( + eventFromEventPusher( + project.NewProjectAddedEvent(context.Background(), + &project.NewAggregate("project1", "org1").Aggregate, + "project"), + ), + eventFromEventPusher( + project.NewProjectRemovedEvent(context.Background(), + &project.NewAggregate("project1", "org1").Aggregate, + "project"), + ), + ), + ), + }, + args: args{ + ctx: context.Background(), + project: &domain.Project{ + ObjectRoot: models.ObjectRoot{ + AggregateID: "project1", + }, + Name: "project change", + }, + resourceOwner: "org1", + }, + res: res{ + err: caos_errs.IsNotFound, + }, + }, + { + name: "no changes, precondition error", + fields: fields{ + eventstore: eventstoreExpect( + t, + expectFilter( + eventFromEventPusher( + project.NewProjectAddedEvent(context.Background(), + &project.NewAggregate("project1", "org1").Aggregate, + "project"), + ), + ), + ), + }, + args: args{ + ctx: context.Background(), + project: &domain.Project{ + ObjectRoot: models.ObjectRoot{ + AggregateID: "project1", + }, + Name: "project", + }, + resourceOwner: "org1", + }, + res: res{ + err: caos_errs.IsPreconditionFailed, + }, + }, + { + name: "project change with name and unique constraints, ok", + fields: fields{ + eventstore: eventstoreExpect( + t, + expectFilter( + eventFromEventPusher( + project.NewProjectAddedEvent(context.Background(), + &project.NewAggregate("project1", "org1").Aggregate, + "project"), + ), + ), + expectPush( + []*repository.Event{ + eventFromEventPusher( + newProjectChangedEvent(context.Background(), + "project1", + "org1", + "project", + "project-new", + true, + true), + ), + }, + uniqueConstraintsFromEventConstraint(project.NewRemoveProjectNameUniqueConstraint("project", "org1")), + uniqueConstraintsFromEventConstraint(project.NewAddProjectNameUniqueConstraint("project-new", "org1")), + ), + ), + }, + args: args{ + ctx: context.Background(), + project: &domain.Project{ + ObjectRoot: models.ObjectRoot{ + AggregateID: "project1", + }, + Name: "project-new", + ProjectRoleAssertion: true, + ProjectRoleCheck: true, + }, + resourceOwner: "org1", + }, + res: res{ + want: &domain.Project{ + ObjectRoot: models.ObjectRoot{ + AggregateID: "project1", + ResourceOwner: "org1", + }, + Name: "project-new", + ProjectRoleAssertion: true, + ProjectRoleCheck: true, + }, + }, + }, + { + name: "project change without name and unique constraints, ok", + fields: fields{ + eventstore: eventstoreExpect( + t, + expectFilter( + eventFromEventPusher( + project.NewProjectAddedEvent(context.Background(), + &project.NewAggregate("project1", "org1").Aggregate, + "project"), + ), + ), + expectPush( + []*repository.Event{ + eventFromEventPusher( + newProjectChangedEvent(context.Background(), + "project1", + "org1", + "project", + "", + true, + true), + ), + }, + ), + ), + }, + args: args{ + ctx: context.Background(), + project: &domain.Project{ + ObjectRoot: models.ObjectRoot{ + AggregateID: "project1", + }, + Name: "project", + ProjectRoleAssertion: true, + ProjectRoleCheck: true, + }, + resourceOwner: "org1", + }, + res: res{ + want: &domain.Project{ + ObjectRoot: models.ObjectRoot{ + AggregateID: "project1", + ResourceOwner: "org1", + }, + Name: "project", + ProjectRoleAssertion: true, + ProjectRoleCheck: true, + }, + }, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + r := &Commands{ + eventstore: tt.fields.eventstore, + } + got, err := r.ChangeProject(tt.args.ctx, tt.args.project, tt.args.resourceOwner) + 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_DeactivateProject(t *testing.T) { + type fields struct { + eventstore *eventstore.Eventstore + } + type args struct { + ctx context.Context + projectID 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 project id, invalid error", + fields: fields{ + eventstore: eventstoreExpect( + t, + ), + }, + args: args{ + ctx: context.Background(), + projectID: "", + resourceOwner: "org1", + }, + res: res{ + err: caos_errs.IsErrorInvalidArgument, + }, + }, + { + name: "invalid resourceowner, invalid error", + fields: fields{ + eventstore: eventstoreExpect( + t, + ), + }, + args: args{ + ctx: context.Background(), + projectID: "project1", + resourceOwner: "", + }, + res: res{ + err: caos_errs.IsErrorInvalidArgument, + }, + }, + { + name: "project not existing, not found error", + fields: fields{ + eventstore: eventstoreExpect( + t, + expectFilter(), + ), + }, + args: args{ + ctx: context.Background(), + projectID: "project1", + resourceOwner: "org1", + }, + res: res{ + err: caos_errs.IsNotFound, + }, + }, + { + name: "project removed, not found error", + fields: fields{ + eventstore: eventstoreExpect( + t, + expectFilter( + eventFromEventPusher( + project.NewProjectAddedEvent(context.Background(), + &project.NewAggregate("project1", "org1").Aggregate, + "project"), + ), + eventFromEventPusher( + project.NewProjectRemovedEvent(context.Background(), + &project.NewAggregate("project1", "org1").Aggregate, + "project"), + ), + ), + ), + }, + args: args{ + ctx: context.Background(), + projectID: "project1", + resourceOwner: "org1", + }, + res: res{ + err: caos_errs.IsNotFound, + }, + }, + { + name: "project already inactive, precondition error", + fields: fields{ + eventstore: eventstoreExpect( + t, + expectFilter( + eventFromEventPusher( + project.NewProjectAddedEvent(context.Background(), + &project.NewAggregate("project1", "org1").Aggregate, + "project"), + ), + eventFromEventPusher( + project.NewProjectDeactivatedEvent(context.Background(), + &project.NewAggregate("project1", "org1").Aggregate), + ), + ), + ), + }, + args: args{ + ctx: context.Background(), + projectID: "project1", + resourceOwner: "org1", + }, + res: res{ + err: caos_errs.IsPreconditionFailed, + }, + }, + { + name: "project deactivate, ok", + fields: fields{ + eventstore: eventstoreExpect( + t, + expectFilter( + eventFromEventPusher( + project.NewProjectAddedEvent(context.Background(), + &project.NewAggregate("project1", "org1").Aggregate, + "project"), + ), + ), + expectPush( + []*repository.Event{ + eventFromEventPusher( + project.NewProjectDeactivatedEvent(context.Background(), + &project.NewAggregate("project1", "org1").Aggregate), + ), + }, + ), + ), + }, + args: args{ + ctx: context.Background(), + projectID: "project1", + resourceOwner: "org1", + }, + res: res{ + want: &domain.ObjectDetails{ + ResourceOwner: "org1", + }, + }, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + r := &Commands{ + eventstore: tt.fields.eventstore, + } + got, err := r.DeactivateProject(tt.args.ctx, tt.args.projectID, tt.args.resourceOwner) + 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_ReactivateProject(t *testing.T) { + type fields struct { + eventstore *eventstore.Eventstore + } + type args struct { + ctx context.Context + projectID 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 project id, invalid error", + fields: fields{ + eventstore: eventstoreExpect( + t, + ), + }, + args: args{ + ctx: context.Background(), + projectID: "", + resourceOwner: "org1", + }, + res: res{ + err: caos_errs.IsErrorInvalidArgument, + }, + }, + { + name: "invalid resourceowner, invalid error", + fields: fields{ + eventstore: eventstoreExpect( + t, + ), + }, + args: args{ + ctx: context.Background(), + projectID: "project1", + resourceOwner: "", + }, + res: res{ + err: caos_errs.IsErrorInvalidArgument, + }, + }, + { + name: "project not existing, not found error", + fields: fields{ + eventstore: eventstoreExpect( + t, + expectFilter(), + ), + }, + args: args{ + ctx: context.Background(), + projectID: "project1", + resourceOwner: "org1", + }, + res: res{ + err: caos_errs.IsNotFound, + }, + }, + { + name: "project removed, not found error", + fields: fields{ + eventstore: eventstoreExpect( + t, + expectFilter( + eventFromEventPusher( + project.NewProjectAddedEvent(context.Background(), + &project.NewAggregate("project1", "org1").Aggregate, + "project"), + ), + eventFromEventPusher( + project.NewProjectRemovedEvent(context.Background(), + &project.NewAggregate("project1", "org1").Aggregate, + "project"), + ), + ), + ), + }, + args: args{ + ctx: context.Background(), + projectID: "project1", + resourceOwner: "org1", + }, + res: res{ + err: caos_errs.IsNotFound, + }, + }, + { + name: "project not inactive, precondition error", + fields: fields{ + eventstore: eventstoreExpect( + t, + expectFilter( + eventFromEventPusher( + project.NewProjectAddedEvent(context.Background(), + &project.NewAggregate("project1", "org1").Aggregate, + "project"), + ), + ), + ), + }, + args: args{ + ctx: context.Background(), + projectID: "project1", + resourceOwner: "org1", + }, + res: res{ + err: caos_errs.IsPreconditionFailed, + }, + }, + { + name: "project reactivate, ok", + fields: fields{ + eventstore: eventstoreExpect( + t, + expectFilter( + eventFromEventPusher( + project.NewProjectAddedEvent(context.Background(), + &project.NewAggregate("project1", "org1").Aggregate, + "project"), + ), + eventFromEventPusher( + project.NewProjectDeactivatedEvent(context.Background(), + &project.NewAggregate("project1", "org1").Aggregate), + ), + ), + expectPush( + []*repository.Event{ + eventFromEventPusher( + project.NewProjectReactivatedEvent(context.Background(), + &project.NewAggregate("project1", "org1").Aggregate), + ), + }, + ), + ), + }, + args: args{ + ctx: context.Background(), + projectID: "project1", + resourceOwner: "org1", + }, + res: res{ + want: &domain.ObjectDetails{ + ResourceOwner: "org1", + }, + }, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + r := &Commands{ + eventstore: tt.fields.eventstore, + } + got, err := r.ReactivateProject(tt.args.ctx, tt.args.projectID, tt.args.resourceOwner) + 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_RemoveProject(t *testing.T) { + type fields struct { + eventstore *eventstore.Eventstore + } + type args struct { + ctx context.Context + projectID 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 project id, invalid error", + fields: fields{ + eventstore: eventstoreExpect( + t, + ), + }, + args: args{ + ctx: context.Background(), + projectID: "", + resourceOwner: "org1", + }, + res: res{ + err: caos_errs.IsErrorInvalidArgument, + }, + }, + { + name: "invalid resourceowner, invalid error", + fields: fields{ + eventstore: eventstoreExpect( + t, + ), + }, + args: args{ + ctx: context.Background(), + projectID: "project1", + resourceOwner: "", + }, + res: res{ + err: caos_errs.IsErrorInvalidArgument, + }, + }, + { + name: "project not existing, not found error", + fields: fields{ + eventstore: eventstoreExpect( + t, + expectFilter(), + ), + }, + args: args{ + ctx: context.Background(), + projectID: "project1", + resourceOwner: "org1", + }, + res: res{ + err: caos_errs.IsNotFound, + }, + }, + { + name: "project removed, not found error", + fields: fields{ + eventstore: eventstoreExpect( + t, + expectFilter( + eventFromEventPusher( + project.NewProjectAddedEvent(context.Background(), + &project.NewAggregate("project1", "org1").Aggregate, + "project"), + ), + eventFromEventPusher( + project.NewProjectRemovedEvent(context.Background(), + &project.NewAggregate("project1", "org1").Aggregate, + "project"), + ), + ), + ), + }, + args: args{ + ctx: context.Background(), + projectID: "project1", + resourceOwner: "org1", + }, + res: res{ + err: caos_errs.IsNotFound, + }, + }, + { + name: "project remove, ok", + fields: fields{ + eventstore: eventstoreExpect( + t, + expectFilter( + eventFromEventPusher( + project.NewProjectAddedEvent(context.Background(), + &project.NewAggregate("project1", "org1").Aggregate, + "project"), + ), + ), + expectPush( + []*repository.Event{ + eventFromEventPusher( + project.NewProjectRemovedEvent(context.Background(), + &project.NewAggregate("project1", "org1").Aggregate, + "project"), + ), + }, + uniqueConstraintsFromEventConstraint(project.NewRemoveProjectNameUniqueConstraint("project", "org1")), + ), + ), + }, + args: args{ + ctx: context.Background(), + projectID: "project1", + resourceOwner: "org1", + }, + res: res{ + want: &domain.ObjectDetails{ + ResourceOwner: "org1", + }, + }, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + r := &Commands{ + eventstore: tt.fields.eventstore, + } + got, err := r.RemoveProject(tt.args.ctx, tt.args.projectID, tt.args.resourceOwner) + 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 newProjectChangedEvent(ctx context.Context, projectID, resourceOwner, oldName, newName string, roleAssertion, roleCheck bool) *project.ProjectChangeEvent { + changes := []project.ProjectChanges{ + project.ChangeProjectRoleAssertion(roleAssertion), + project.ChangeProjectRoleCheck(roleCheck), + } + if newName != "" { + changes = append(changes, project.ChangeName(newName)) + } + event, _ := project.NewProjectChangeEvent(ctx, + &project.NewAggregate(projectID, resourceOwner).Aggregate, + oldName, + changes, + ) + return event +} diff --git a/internal/command/user_grant.go b/internal/command/user_grant.go index 94e2600c6..fe70d0b9b 100644 --- a/internal/command/user_grant.go +++ b/internal/command/user_grant.go @@ -34,13 +34,9 @@ func (c *Commands) addUserGrant(ctx context.Context, userGrant *domain.UserGrant return nil, nil, err } if !userGrant.IsValid() { - return nil, nil, caos_errs.ThrowPreconditionFailed(nil, "COMMAND-4M0fs", "Errors.UserGrant.Invalid") + return nil, nil, caos_errs.ThrowInvalidArgument(nil, "COMMAND-4M0fs", "Errors.UserGrant.Invalid") } - err = c.checkUserExists(ctx, userGrant.UserID, "") - if err != nil { - return nil, nil, err - } - err = c.checkProjectExists(ctx, userGrant.ProjectID, resourceOwner) + err = c.checkUserGrantPreCondition(ctx, userGrant) if err != nil { return nil, nil, err } @@ -83,10 +79,14 @@ func (c *Commands) changeUserGrant(ctx context.Context, userGrant *domain.UserGr if err != nil { return nil, nil, err } - if userGrant.IsValid() { - return nil, nil, caos_errs.ThrowPreconditionFailed(nil, "COMMAND-3M0sd", "Errors.UserGrant.Invalid") + if !userGrant.IsValid() || userGrant.AggregateID == "" { + return nil, nil, caos_errs.ThrowInvalidArgument(nil, "COMMAND-3M0sd", "Errors.UserGrant.Invalid") } + err = c.checkUserGrantPreCondition(ctx, userGrant) + if err != nil { + return nil, nil, err + } existingUserGrant, err := c.userGrantWriteModelByID(ctx, userGrant.AggregateID, userGrant.ResourceOwner) if err != nil { return nil, nil, err @@ -130,7 +130,7 @@ func (c *Commands) removeRoleFromUserGrant(ctx context.Context, userGrantID stri if !keyExists { return nil, caos_errs.ThrowPreconditionFailed(nil, "COMMAND-5m8g9", "Errors.UserGrant.RoleKeyNotFound") } - changedUserGrant := NewUserGrantWriteModel(userGrantID, "") + changedUserGrant := NewUserGrantWriteModel(userGrantID, existingUserGrant.ResourceOwner) userGrantAgg := UserGrantAggregateFromWriteModel(&changedUserGrant.WriteModel) if cascade { @@ -142,22 +142,22 @@ func (c *Commands) removeRoleFromUserGrant(ctx context.Context, userGrantID stri func (c *Commands) DeactivateUserGrant(ctx context.Context, grantID, resourceOwner string) (objectDetails *domain.ObjectDetails, err error) { if grantID == "" || resourceOwner == "" { - return nil, caos_errs.ThrowPreconditionFailed(nil, "COMMAND-M0dsf", "Errors.UserGrant.IDMissing") + return nil, caos_errs.ThrowInvalidArgument(nil, "COMMAND-M0dsf", "Errors.UserGrant.IDMissing") } existingUserGrant, err := c.userGrantWriteModelByID(ctx, grantID, resourceOwner) if err != nil { return nil, err } - err = checkExplicitProjectPermission(ctx, existingUserGrant.ProjectGrantID, existingUserGrant.ProjectID) - if err != nil { - return nil, err - } if existingUserGrant.State == domain.UserGrantStateUnspecified || existingUserGrant.State == domain.UserGrantStateRemoved { return nil, caos_errs.ThrowNotFound(nil, "COMMAND-3M9sd", "Errors.UserGrant.NotFound") } if existingUserGrant.State != domain.UserGrantStateActive { - return nil, caos_errs.ThrowNotFound(nil, "COMMAND-1S9gx", "Errors.UserGrant.NotActive") + return nil, caos_errs.ThrowPreconditionFailed(nil, "COMMAND-1S9gx", "Errors.UserGrant.NotActive") + } + err = checkExplicitProjectPermission(ctx, existingUserGrant.ProjectGrantID, existingUserGrant.ProjectID) + if err != nil { + return nil, err } deactivateUserGrant := NewUserGrantWriteModel(grantID, resourceOwner) @@ -175,24 +175,23 @@ func (c *Commands) DeactivateUserGrant(ctx context.Context, grantID, resourceOwn func (c *Commands) ReactivateUserGrant(ctx context.Context, grantID, resourceOwner string) (objectDetails *domain.ObjectDetails, err error) { if grantID == "" || resourceOwner == "" { - return nil, caos_errs.ThrowPreconditionFailed(nil, "COMMAND-Qxy8v", "Errors.UserGrant.IDMissing") + return nil, caos_errs.ThrowInvalidArgument(nil, "COMMAND-Qxy8v", "Errors.UserGrant.IDMissing") } existingUserGrant, err := c.userGrantWriteModelByID(ctx, grantID, resourceOwner) if err != nil { return nil, err } - err = checkExplicitProjectPermission(ctx, existingUserGrant.ProjectGrantID, existingUserGrant.ProjectID) - if err != nil { - return nil, err - } if existingUserGrant.State == domain.UserGrantStateUnspecified || existingUserGrant.State == domain.UserGrantStateRemoved { return nil, caos_errs.ThrowNotFound(nil, "COMMAND-Lp0gs", "Errors.UserGrant.NotFound") } if existingUserGrant.State != domain.UserGrantStateInactive { - return nil, caos_errs.ThrowNotFound(nil, "COMMAND-1ML0v", "Errors.UserGrant.NotInactive") + return nil, caos_errs.ThrowPreconditionFailed(nil, "COMMAND-1ML0v", "Errors.UserGrant.NotInactive") + } + err = checkExplicitProjectPermission(ctx, existingUserGrant.ProjectGrantID, existingUserGrant.ProjectID) + if err != nil { + return nil, err } - deactivateUserGrant := NewUserGrantWriteModel(grantID, resourceOwner) userGrantAgg := UserGrantAggregateFromWriteModel(&deactivateUserGrant.WriteModel) pushedEvents, err := c.eventstore.PushEvents(ctx, usergrant.NewUserGrantReactivatedEvent(ctx, userGrantAgg)) @@ -224,11 +223,14 @@ func (c *Commands) RemoveUserGrant(ctx context.Context, grantID, resourceOwner s } func (c *Commands) BulkRemoveUserGrant(ctx context.Context, grantIDs []string, resourceOwner string) (err error) { + if len(grantIDs) == 0 { + return caos_errs.ThrowInvalidArgument(nil, "COMMAND-5M0sd", "Errors.UserGrant.IDMissing") + } events := make([]eventstore.EventPusher, len(grantIDs)) for i, grantID := range grantIDs { event, _, err := c.removeUserGrant(ctx, grantID, resourceOwner, false) if err != nil { - return nil + return err } events[i] = event } @@ -238,13 +240,16 @@ func (c *Commands) BulkRemoveUserGrant(ctx context.Context, grantIDs []string, r func (c *Commands) removeUserGrant(ctx context.Context, grantID, resourceOwner string, cascade bool) (_ eventstore.EventPusher, writeModel *UserGrantWriteModel, err error) { if grantID == "" { - return nil, nil, caos_errs.ThrowPreconditionFailed(nil, "COMMAND-J9sc5", "Errors.UserGrant.IDMissing") + return nil, nil, caos_errs.ThrowInvalidArgument(nil, "COMMAND-J9sc5", "Errors.UserGrant.IDMissing") } existingUserGrant, err := c.userGrantWriteModelByID(ctx, grantID, resourceOwner) if err != nil { return nil, nil, err } + if existingUserGrant.State == domain.UserGrantStateUnspecified || existingUserGrant.State == domain.UserGrantStateRemoved { + return nil, nil, caos_errs.ThrowNotFound(nil, "COMMAND-1My0t", "Errors.UserGrant.NotFound") + } if !cascade { err = checkExplicitProjectPermission(ctx, existingUserGrant.ProjectGrantID, existingUserGrant.ProjectID) if err != nil { @@ -252,11 +257,7 @@ func (c *Commands) removeUserGrant(ctx context.Context, grantID, resourceOwner s } } - if existingUserGrant.State == domain.UserGrantStateUnspecified || existingUserGrant.State == domain.UserGrantStateRemoved { - return nil, nil, caos_errs.ThrowNotFound(nil, "COMMAND-1My0t", "Errors.UserGrant.NotFound") - } - - removeUserGrant := NewUserGrantWriteModel(grantID, resourceOwner) + removeUserGrant := NewUserGrantWriteModel(grantID, existingUserGrant.ResourceOwner) userGrantAgg := UserGrantAggregateFromWriteModel(&removeUserGrant.WriteModel) if cascade { return usergrant.NewUserGrantCascadeRemovedEvent( @@ -273,6 +274,7 @@ func (c *Commands) removeUserGrant(ctx context.Context, grantID, resourceOwner s existingUserGrant.ProjectID, existingUserGrant.ProjectGrantID), existingUserGrant, nil } + func (c *Commands) userGrantWriteModelByID(ctx context.Context, userGrantID, resourceOwner string) (writeModel *UserGrantWriteModel, err error) { ctx, span := tracing.NewSpan(ctx) defer func() { span.EndWithError(err) }() @@ -284,3 +286,24 @@ func (c *Commands) userGrantWriteModelByID(ctx context.Context, userGrantID, res } return writeModel, nil } + +func (c *Commands) checkUserGrantPreCondition(ctx context.Context, usergrant *domain.UserGrant) error { + preConditions := NewUserGrantPreConditionReadModel(usergrant.UserID, usergrant.ProjectID, usergrant.ProjectGrantID) + err := c.eventstore.FilterToQueryReducer(ctx, preConditions) + if err != nil { + return err + } + if !preConditions.UserExists { + return caos_errs.ThrowPreconditionFailed(err, "COMMAND-4f8sg", "Errors.User.NotFound") + } + if !preConditions.ProjectExists { + return caos_errs.ThrowPreconditionFailed(err, "COMMAND-3n77S", "Errors.Project.NotFound") + } + if usergrant.ProjectGrantID != "" && !preConditions.ProjectGrantExists { + return caos_errs.ThrowPreconditionFailed(err, "COMMAND-4m9ff", "Errors.Project.Grant.NotFound") + } + if usergrant.HasInvalidRoles(preConditions.ExistingRoleKeys) { + return caos_errs.ThrowPreconditionFailed(err, "COMMAND-mm9F4", "Errors.Project.Role.NotFound") + } + return nil +} diff --git a/internal/command/user_grant_model.go b/internal/command/user_grant_model.go index 6421d64f8..c4e6378df 100644 --- a/internal/command/user_grant_model.go +++ b/internal/command/user_grant_model.go @@ -3,6 +3,8 @@ package command import ( "github.com/caos/zitadel/internal/domain" "github.com/caos/zitadel/internal/eventstore" + "github.com/caos/zitadel/internal/repository/project" + "github.com/caos/zitadel/internal/repository/user" "github.com/caos/zitadel/internal/repository/usergrant" ) @@ -76,3 +78,85 @@ func (wm *UserGrantWriteModel) Query() *eventstore.SearchQueryBuilder { func UserGrantAggregateFromWriteModel(wm *eventstore.WriteModel) *eventstore.Aggregate { return eventstore.AggregateFromWriteModel(wm, usergrant.AggregateType, usergrant.AggregateVersion) } + +type UserGrantPreConditionReadModel struct { + eventstore.WriteModel + + UserID string + ProjectID string + ProjectGrantID string + UserExists bool + ProjectExists bool + ProjectGrantExists bool + ExistingRoleKeys []string +} + +func NewUserGrantPreConditionReadModel(userID, projectID, projectGrantID string) *UserGrantPreConditionReadModel { + return &UserGrantPreConditionReadModel{ + UserID: userID, + ProjectID: projectID, + ProjectGrantID: projectGrantID, + } +} + +func (wm *UserGrantPreConditionReadModel) Reduce() error { + for _, event := range wm.Events { + switch e := event.(type) { + case *user.HumanAddedEvent: + wm.UserExists = true + case *user.MachineAddedEvent: + wm.UserExists = true + case *user.UserRemovedEvent: + wm.UserExists = false + case *project.ProjectAddedEvent: + wm.ProjectExists = true + case *project.ProjectRemovedEvent: + wm.ProjectExists = false + case *project.GrantAddedEvent: + if wm.ProjectGrantID == e.GrantID { + wm.ProjectGrantExists = true + } + wm.ExistingRoleKeys = e.RoleKeys + case *project.GrantChangedEvent: + wm.ExistingRoleKeys = e.RoleKeys + case *project.GrantRemovedEvent: + if wm.ProjectGrantID == e.GrantID { + wm.ProjectGrantExists = false + } + wm.ExistingRoleKeys = []string{} + case *project.RoleAddedEvent: + if wm.ProjectGrantID != "" { + continue + } + wm.ExistingRoleKeys = append(wm.ExistingRoleKeys, e.Key) + case *project.RoleRemovedEvent: + if wm.ProjectGrantID != "" { + continue + } + for i, key := range wm.ExistingRoleKeys { + if key == e.Key { + copy(wm.ExistingRoleKeys[i:], wm.ExistingRoleKeys[i+1:]) + wm.ExistingRoleKeys[len(wm.ExistingRoleKeys)-1] = "" + wm.ExistingRoleKeys = wm.ExistingRoleKeys[:len(wm.ExistingRoleKeys)-1] + continue + } + } + } + } + return wm.WriteModel.Reduce() +} + +func (wm *UserGrantPreConditionReadModel) Query() *eventstore.SearchQueryBuilder { + query := eventstore.NewSearchQueryBuilder(eventstore.ColumnsEvent, user.AggregateType, project.AggregateType). + AggregateIDs(wm.UserID, wm.ProjectID). + EventTypes(user.HumanAddedType, + user.MachineAddedEventType, + user.UserRemovedType, + project.ProjectAddedType, + project.ProjectRemovedType, + project.GrantAddedType, + project.GrantRemovedType, + project.RoleAddedType, + project.RoleRemovedType) + return query +} diff --git a/internal/command/user_grant_test.go b/internal/command/user_grant_test.go new file mode 100644 index 000000000..30b6159d3 --- /dev/null +++ b/internal/command/user_grant_test.go @@ -0,0 +1,2027 @@ +package command + +import ( + "context" + "github.com/caos/zitadel/internal/api/authz" + "github.com/caos/zitadel/internal/domain" + caos_errs "github.com/caos/zitadel/internal/errors" + "github.com/caos/zitadel/internal/eventstore" + "github.com/caos/zitadel/internal/eventstore/repository" + "github.com/caos/zitadel/internal/eventstore/v1/models" + "github.com/caos/zitadel/internal/id" + id_mock "github.com/caos/zitadel/internal/id/mock" + "github.com/caos/zitadel/internal/repository/project" + "github.com/caos/zitadel/internal/repository/user" + "github.com/caos/zitadel/internal/repository/usergrant" + "github.com/stretchr/testify/assert" + "golang.org/x/text/language" + "testing" +) + +func TestCommandSide_AddUserGrant(t *testing.T) { + type fields struct { + eventstore *eventstore.Eventstore + idGenerator id.Generator + } + type args struct { + ctx context.Context + userGrant *domain.UserGrant + resourceOwner string + } + type res struct { + want *domain.UserGrant + err func(error) bool + } + tests := []struct { + name string + fields fields + args args + res res + }{ + { + name: "invalid permissions, error", + fields: fields{ + eventstore: eventstoreExpect( + t, + ), + }, + args: args{ + ctx: context.Background(), + userGrant: &domain.UserGrant{ + UserID: "user1", + }, + resourceOwner: "org1", + }, + res: res{ + err: caos_errs.IsPermissionDenied, + }, + }, + { + name: "invalid usergrant, error", + fields: fields{ + eventstore: eventstoreExpect( + t, + ), + }, + args: args{ + ctx: authz.NewMockContextWithPermissions("org", "user", []string{domain.RoleProjectOwner}), + userGrant: &domain.UserGrant{ + UserID: "user1", + }, + resourceOwner: "org1", + }, + res: res{ + err: caos_errs.IsErrorInvalidArgument, + }, + }, + { + name: "user removed, precondition error", + fields: fields{ + eventstore: eventstoreExpect( + t, + expectFilter( + eventFromEventPusher( + user.NewHumanAddedEvent(context.Background(), + &user.NewAggregate("user1", "org1").Aggregate, + "username1", + "firstname1", + "lastname1", + "nickname1", + "displayname1", + language.German, + domain.GenderMale, + "email1", + true, + ), + ), + eventFromEventPusher( + user.NewUserRemovedEvent( + context.Background(), + &user.NewAggregate("user1", "org1").Aggregate, + "username1", + true, + ), + ), + ), + ), + }, + args: args{ + ctx: authz.NewMockContextWithPermissions("org", "user", []string{domain.RoleProjectOwner}), + userGrant: &domain.UserGrant{ + UserID: "user1", + ProjectID: "project1", + }, + resourceOwner: "org1", + }, + res: res{ + err: caos_errs.IsPreconditionFailed, + }, + }, + { + name: "project removed, precondition error", + fields: fields{ + eventstore: eventstoreExpect( + t, + expectFilter( + eventFromEventPusher( + user.NewHumanAddedEvent(context.Background(), + &user.NewAggregate("user1", "org1").Aggregate, + "username1", + "firstname1", + "lastname1", + "nickname1", + "displayname1", + language.German, + domain.GenderMale, + "email1", + true, + ), + ), + eventFromEventPusher( + project.NewProjectAddedEvent(context.Background(), + &project.NewAggregate("project1", "org1").Aggregate, + "projectname1", + ), + ), + eventFromEventPusher( + project.NewProjectRemovedEvent(context.Background(), + &project.NewAggregate("project1", "org1").Aggregate, + "projectname1", + ), + ), + ), + ), + }, + args: args{ + ctx: authz.NewMockContextWithPermissions("org", "user", []string{domain.RoleProjectOwner}), + userGrant: &domain.UserGrant{ + UserID: "user1", + ProjectID: "project1", + }, + resourceOwner: "org1", + }, + res: res{ + err: caos_errs.IsPreconditionFailed, + }, + }, + { + name: "project roles not existing, precondition error", + fields: fields{ + eventstore: eventstoreExpect( + t, + expectFilter( + eventFromEventPusher( + user.NewHumanAddedEvent(context.Background(), + &user.NewAggregate("user1", "org1").Aggregate, + "username1", + "firstname1", + "lastname1", + "nickname1", + "displayname1", + language.German, + domain.GenderMale, + "email1", + true, + ), + ), + eventFromEventPusher( + project.NewProjectAddedEvent(context.Background(), + &project.NewAggregate("project1", "org1").Aggregate, + "projectname1", + ), + ), + ), + ), + }, + args: args{ + ctx: authz.NewMockContextWithPermissions("org", "user", []string{domain.RoleProjectOwner}), + userGrant: &domain.UserGrant{ + UserID: "user1", + ProjectID: "project1", + RoleKeys: []string{"roleKey"}, + }, + resourceOwner: "org1", + }, + res: res{ + err: caos_errs.IsPreconditionFailed, + }, + }, + { + name: "project grant not existing, precondition error", + fields: fields{ + eventstore: eventstoreExpect( + t, + expectFilter( + eventFromEventPusher( + user.NewHumanAddedEvent(context.Background(), + &user.NewAggregate("user1", "org1").Aggregate, + "username1", + "firstname1", + "lastname1", + "nickname1", + "displayname1", + language.German, + domain.GenderMale, + "email1", + true, + ), + ), + eventFromEventPusher( + project.NewProjectAddedEvent(context.Background(), + &project.NewAggregate("project1", "org1").Aggregate, + "projectname1", + ), + ), + ), + ), + }, + args: args{ + ctx: authz.NewMockContextWithPermissions("org", "user", []string{domain.RoleProjectOwner}), + userGrant: &domain.UserGrant{ + UserID: "user1", + ProjectID: "project1", + ProjectGrantID: "projectgrant1", + RoleKeys: []string{"roleKey"}, + }, + resourceOwner: "org1", + }, + res: res{ + err: caos_errs.IsPreconditionFailed, + }, + }, + { + name: "project grant roles not existing, precondition error", + fields: fields{ + eventstore: eventstoreExpect( + t, + expectFilter( + eventFromEventPusher( + user.NewHumanAddedEvent(context.Background(), + &user.NewAggregate("user1", "org1").Aggregate, + "username1", + "firstname1", + "lastname1", + "nickname1", + "displayname1", + language.German, + domain.GenderMale, + "email1", + true, + ), + ), + eventFromEventPusher( + project.NewProjectAddedEvent(context.Background(), + &project.NewAggregate("project1", "org1").Aggregate, + "projectname1", + ), + ), + eventFromEventPusher( + project.NewRoleAddedEvent(context.Background(), + &project.NewAggregate("project1", "org1").Aggregate, + "rolekey1", + "rolekey", + "", + ), + ), + eventFromEventPusher( + project.NewGrantAddedEvent(context.Background(), + &project.NewAggregate("project1", "org1").Aggregate, + "projectgrant1", + "org2", + nil, + ), + ), + ), + ), + }, + args: args{ + ctx: authz.NewMockContextWithPermissions("org", "user", []string{domain.RoleProjectOwner}), + userGrant: &domain.UserGrant{ + UserID: "user1", + ProjectID: "project1", + ProjectGrantID: "projectgrant1", + RoleKeys: []string{"roleKey"}, + }, + resourceOwner: "org1", + }, + res: res{ + err: caos_errs.IsPreconditionFailed, + }, + }, + { + name: "usergrant for project, ok", + fields: fields{ + eventstore: eventstoreExpect( + t, + expectFilter( + eventFromEventPusher( + user.NewHumanAddedEvent(context.Background(), + &user.NewAggregate("user1", "org1").Aggregate, + "username1", + "firstname1", + "lastname1", + "nickname1", + "displayname1", + language.German, + domain.GenderMale, + "email1", + true, + ), + ), + eventFromEventPusher( + project.NewProjectAddedEvent(context.Background(), + &project.NewAggregate("project1", "org1").Aggregate, + "projectname1", + ), + ), + eventFromEventPusher( + project.NewRoleAddedEvent(context.Background(), + &project.NewAggregate("project1", "org1").Aggregate, + "rolekey1", + "rolekey", + "", + ), + ), + ), + expectPush( + []*repository.Event{ + eventFromEventPusher(usergrant.NewUserGrantAddedEvent(context.Background(), + &usergrant.NewAggregate("usergrant1", "org1").Aggregate, + "user1", + "project1", + "", + []string{"rolekey1"}, + )), + }, + uniqueConstraintsFromEventConstraint(usergrant.NewAddUserGrantUniqueConstraint("org1", "user1", "project1", "")), + ), + ), + idGenerator: id_mock.NewIDGeneratorExpectIDs(t, "usergrant1"), + }, + args: args{ + ctx: authz.NewMockContextWithPermissions("", "", []string{domain.RoleProjectOwner}), + userGrant: &domain.UserGrant{ + UserID: "user1", + ProjectID: "project1", + RoleKeys: []string{"rolekey1"}, + }, + resourceOwner: "org1", + }, + res: res{ + want: &domain.UserGrant{ + ObjectRoot: models.ObjectRoot{ + AggregateID: "usergrant1", + ResourceOwner: "org1", + }, + UserID: "user1", + ProjectID: "project1", + RoleKeys: []string{"rolekey1"}, + State: domain.UserGrantStateActive, + }, + }, + }, + { + name: "usergrant for projectgrant, ok", + fields: fields{ + eventstore: eventstoreExpect( + t, + expectFilter( + eventFromEventPusher( + user.NewHumanAddedEvent(context.Background(), + &user.NewAggregate("user1", "org1").Aggregate, + "username1", + "firstname1", + "lastname1", + "nickname1", + "displayname1", + language.German, + domain.GenderMale, + "email1", + true, + ), + ), + eventFromEventPusher( + project.NewProjectAddedEvent(context.Background(), + &project.NewAggregate("project1", "org1").Aggregate, + "projectname1", + ), + ), + eventFromEventPusher( + project.NewRoleAddedEvent(context.Background(), + &project.NewAggregate("project1", "org1").Aggregate, + "rolekey1", + "rolekey", + "", + ), + ), + eventFromEventPusher( + project.NewGrantAddedEvent(context.Background(), + &project.NewAggregate("project1", "org1").Aggregate, + "projectgrant1", + "org1", + []string{"rolekey1"}, + ), + ), + ), + expectPush( + []*repository.Event{ + eventFromEventPusher(usergrant.NewUserGrantAddedEvent(context.Background(), + &usergrant.NewAggregate("usergrant1", "org1").Aggregate, + "user1", + "project1", + "projectgrant1", + []string{"rolekey1"}, + )), + }, + uniqueConstraintsFromEventConstraint(usergrant.NewAddUserGrantUniqueConstraint("org1", "user1", "project1", "projectgrant1")), + ), + ), + idGenerator: id_mock.NewIDGeneratorExpectIDs(t, "usergrant1"), + }, + args: args{ + ctx: authz.NewMockContextWithPermissions("", "", []string{domain.RoleProjectOwner}), + userGrant: &domain.UserGrant{ + UserID: "user1", + ProjectID: "project1", + ProjectGrantID: "projectgrant1", + RoleKeys: []string{"rolekey1"}, + }, + resourceOwner: "org1", + }, + res: res{ + want: &domain.UserGrant{ + ObjectRoot: models.ObjectRoot{ + AggregateID: "usergrant1", + ResourceOwner: "org1", + }, + UserID: "user1", + ProjectID: "project1", + ProjectGrantID: "projectgrant1", + RoleKeys: []string{"rolekey1"}, + State: domain.UserGrantStateActive, + }, + }, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + r := &Commands{ + eventstore: tt.fields.eventstore, + idGenerator: tt.fields.idGenerator, + } + got, err := r.AddUserGrant(tt.args.ctx, tt.args.userGrant, tt.args.resourceOwner) + 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_ChangeUserGrant(t *testing.T) { + type fields struct { + eventstore *eventstore.Eventstore + } + type args struct { + ctx context.Context + userGrant *domain.UserGrant + resourceOwner string + } + type res struct { + want *domain.UserGrant + err func(error) bool + } + tests := []struct { + name string + fields fields + args args + res res + }{ + { + name: "invalid permissions, error", + fields: fields{ + eventstore: eventstoreExpect( + t, + ), + }, + args: args{ + ctx: context.Background(), + userGrant: &domain.UserGrant{ + UserID: "user1", + }, + resourceOwner: "org1", + }, + res: res{ + err: caos_errs.IsPermissionDenied, + }, + }, + { + name: "invalid usergrant, error", + fields: fields{ + eventstore: eventstoreExpect( + t, + ), + }, + args: args{ + ctx: authz.NewMockContextWithPermissions("org", "user", []string{domain.RoleProjectOwner}), + userGrant: &domain.UserGrant{ + UserID: "user1", + }, + resourceOwner: "org1", + }, + res: res{ + err: caos_errs.IsErrorInvalidArgument, + }, + }, + { + name: "user removed, precondition error", + fields: fields{ + eventstore: eventstoreExpect( + t, + expectFilter( + eventFromEventPusher( + user.NewHumanAddedEvent(context.Background(), + &user.NewAggregate("user1", "org1").Aggregate, + "username1", + "firstname1", + "lastname1", + "nickname1", + "displayname1", + language.German, + domain.GenderMale, + "email1", + true, + ), + ), + eventFromEventPusher( + user.NewUserRemovedEvent( + context.Background(), + &user.NewAggregate("user1", "org1").Aggregate, + "username1", + true, + ), + ), + ), + ), + }, + args: args{ + ctx: authz.NewMockContextWithPermissions("org", "user", []string{domain.RoleProjectOwner}), + userGrant: &domain.UserGrant{ + ObjectRoot: models.ObjectRoot{ + AggregateID: "usergrant1", + }, + UserID: "user1", + ProjectID: "project1", + }, + resourceOwner: "org1", + }, + res: res{ + err: caos_errs.IsPreconditionFailed, + }, + }, + { + name: "project removed, precondition error", + fields: fields{ + eventstore: eventstoreExpect( + t, + expectFilter( + eventFromEventPusher( + user.NewHumanAddedEvent(context.Background(), + &user.NewAggregate("user1", "org1").Aggregate, + "username1", + "firstname1", + "lastname1", + "nickname1", + "displayname1", + language.German, + domain.GenderMale, + "email1", + true, + ), + ), + eventFromEventPusher( + project.NewProjectAddedEvent(context.Background(), + &project.NewAggregate("project1", "org1").Aggregate, + "projectname1", + ), + ), + eventFromEventPusher( + project.NewProjectRemovedEvent(context.Background(), + &project.NewAggregate("project1", "org1").Aggregate, + "projectname1", + ), + ), + ), + ), + }, + args: args{ + ctx: authz.NewMockContextWithPermissions("org", "user", []string{domain.RoleProjectOwner}), + userGrant: &domain.UserGrant{ + ObjectRoot: models.ObjectRoot{ + AggregateID: "usergrant1", + }, + UserID: "user1", + ProjectID: "project1", + }, + resourceOwner: "org1", + }, + res: res{ + err: caos_errs.IsPreconditionFailed, + }, + }, + { + name: "project roles not existing, precondition error", + fields: fields{ + eventstore: eventstoreExpect( + t, + expectFilter( + eventFromEventPusher( + user.NewHumanAddedEvent(context.Background(), + &user.NewAggregate("user1", "org1").Aggregate, + "username1", + "firstname1", + "lastname1", + "nickname1", + "displayname1", + language.German, + domain.GenderMale, + "email1", + true, + ), + ), + eventFromEventPusher( + project.NewProjectAddedEvent(context.Background(), + &project.NewAggregate("project1", "org1").Aggregate, + "projectname1", + ), + ), + ), + ), + }, + args: args{ + ctx: authz.NewMockContextWithPermissions("org", "user", []string{domain.RoleProjectOwner}), + userGrant: &domain.UserGrant{ + ObjectRoot: models.ObjectRoot{ + AggregateID: "usergrant1", + }, + UserID: "user1", + ProjectID: "project1", + RoleKeys: []string{"roleKey"}, + }, + resourceOwner: "org1", + }, + res: res{ + err: caos_errs.IsPreconditionFailed, + }, + }, + { + name: "project grant not existing, precondition error", + fields: fields{ + eventstore: eventstoreExpect( + t, + expectFilter( + eventFromEventPusher( + user.NewHumanAddedEvent(context.Background(), + &user.NewAggregate("user1", "org1").Aggregate, + "username1", + "firstname1", + "lastname1", + "nickname1", + "displayname1", + language.German, + domain.GenderMale, + "email1", + true, + ), + ), + eventFromEventPusher( + project.NewProjectAddedEvent(context.Background(), + &project.NewAggregate("project1", "org1").Aggregate, + "projectname1", + ), + ), + ), + ), + }, + args: args{ + ctx: authz.NewMockContextWithPermissions("org", "user", []string{domain.RoleProjectOwner}), + userGrant: &domain.UserGrant{ + ObjectRoot: models.ObjectRoot{ + AggregateID: "usergrant1", + }, + UserID: "user1", + ProjectID: "project1", + ProjectGrantID: "projectgrant1", + RoleKeys: []string{"roleKey"}, + }, + resourceOwner: "org1", + }, + res: res{ + err: caos_errs.IsPreconditionFailed, + }, + }, + { + name: "project grant roles not existing, precondition error", + fields: fields{ + eventstore: eventstoreExpect( + t, + expectFilter( + eventFromEventPusher( + user.NewHumanAddedEvent(context.Background(), + &user.NewAggregate("user1", "org1").Aggregate, + "username1", + "firstname1", + "lastname1", + "nickname1", + "displayname1", + language.German, + domain.GenderMale, + "email1", + true, + ), + ), + eventFromEventPusher( + project.NewProjectAddedEvent(context.Background(), + &project.NewAggregate("project1", "org1").Aggregate, + "projectname1", + ), + ), + eventFromEventPusher( + project.NewRoleAddedEvent(context.Background(), + &project.NewAggregate("project1", "org1").Aggregate, + "rolekey1", + "rolekey", + "", + ), + ), + eventFromEventPusher( + project.NewGrantAddedEvent(context.Background(), + &project.NewAggregate("project1", "org1").Aggregate, + "projectgrant1", + "org2", + nil, + ), + ), + ), + ), + }, + args: args{ + ctx: authz.NewMockContextWithPermissions("org", "user", []string{domain.RoleProjectOwner}), + userGrant: &domain.UserGrant{ + ObjectRoot: models.ObjectRoot{ + AggregateID: "usergrant1", + }, + UserID: "user1", + ProjectID: "project1", + ProjectGrantID: "projectgrant1", + RoleKeys: []string{"roleKey"}, + }, + resourceOwner: "org1", + }, + res: res{ + err: caos_errs.IsPreconditionFailed, + }, + }, + { + name: "usergrant not existing, not found error", + fields: fields{ + eventstore: eventstoreExpect( + t, + expectFilter( + eventFromEventPusher( + user.NewHumanAddedEvent(context.Background(), + &user.NewAggregate("user1", "org1").Aggregate, + "username1", + "firstname1", + "lastname1", + "nickname1", + "displayname1", + language.German, + domain.GenderMale, + "email1", + true, + ), + ), + eventFromEventPusher( + project.NewProjectAddedEvent(context.Background(), + &project.NewAggregate("project1", "org1").Aggregate, + "projectname1", + ), + ), + eventFromEventPusher( + project.NewRoleAddedEvent(context.Background(), + &project.NewAggregate("project1", "org1").Aggregate, + "rolekey1", + "rolekey", + "", + ), + ), + ), + expectFilter(), + ), + }, + args: args{ + ctx: authz.NewMockContextWithPermissions("", "", []string{domain.RoleProjectOwner}), + userGrant: &domain.UserGrant{ + ObjectRoot: models.ObjectRoot{ + AggregateID: "usergrant1", + }, + UserID: "user1", + ProjectID: "project1", + RoleKeys: []string{"rolekey1"}, + }, + resourceOwner: "org1", + }, + res: res{ + err: caos_errs.IsNotFound, + }, + }, + { + name: "usergrant not existing, not found error", + fields: fields{ + eventstore: eventstoreExpect( + t, + expectFilter( + eventFromEventPusher( + user.NewHumanAddedEvent(context.Background(), + &user.NewAggregate("user1", "org1").Aggregate, + "username1", + "firstname1", + "lastname1", + "nickname1", + "displayname1", + language.German, + domain.GenderMale, + "email1", + true, + ), + ), + eventFromEventPusher( + project.NewProjectAddedEvent(context.Background(), + &project.NewAggregate("project1", "org1").Aggregate, + "projectname1", + ), + ), + eventFromEventPusher( + project.NewRoleAddedEvent(context.Background(), + &project.NewAggregate("project1", "org1").Aggregate, + "rolekey1", + "rolekey", + "", + ), + ), + ), + expectFilter(), + ), + }, + args: args{ + ctx: authz.NewMockContextWithPermissions("", "", []string{domain.RoleProjectOwner}), + userGrant: &domain.UserGrant{ + ObjectRoot: models.ObjectRoot{ + AggregateID: "usergrant1", + }, + UserID: "user1", + ProjectID: "project1", + RoleKeys: []string{"rolekey1"}, + }, + resourceOwner: "org1", + }, + res: res{ + err: caos_errs.IsNotFound, + }, + }, + { + name: "usergrant roles not changed, precondition error", + fields: fields{ + eventstore: eventstoreExpect( + t, + expectFilter( + eventFromEventPusher( + user.NewHumanAddedEvent(context.Background(), + &user.NewAggregate("user1", "org1").Aggregate, + "username1", + "firstname1", + "lastname1", + "nickname1", + "displayname1", + language.German, + domain.GenderMale, + "email1", + true, + ), + ), + eventFromEventPusher( + project.NewProjectAddedEvent(context.Background(), + &project.NewAggregate("project1", "org1").Aggregate, + "projectname1", + ), + ), + eventFromEventPusher( + project.NewRoleAddedEvent(context.Background(), + &project.NewAggregate("project1", "org1").Aggregate, + "rolekey1", + "rolekey", + "", + ), + ), + ), + expectFilter( + eventFromEventPusher( + usergrant.NewUserGrantAddedEvent(context.Background(), + &usergrant.NewAggregate("usergrant1", "org").Aggregate, + "user1", + "project1", + "", []string{"rolekey1"}), + ), + ), + ), + }, + args: args{ + ctx: authz.NewMockContextWithPermissions("", "", []string{domain.RoleProjectOwner}), + userGrant: &domain.UserGrant{ + ObjectRoot: models.ObjectRoot{ + AggregateID: "usergrant1", + }, + UserID: "user1", + ProjectID: "project1", + RoleKeys: []string{"rolekey1"}, + }, + resourceOwner: "org1", + }, + res: res{ + err: caos_errs.IsPreconditionFailed, + }, + }, + { + name: "usergrant for project, ok", + fields: fields{ + eventstore: eventstoreExpect( + t, + expectFilter( + eventFromEventPusher( + user.NewHumanAddedEvent(context.Background(), + &user.NewAggregate("user1", "org1").Aggregate, + "username1", + "firstname1", + "lastname1", + "nickname1", + "displayname1", + language.German, + domain.GenderMale, + "email1", + true, + ), + ), + eventFromEventPusher( + project.NewProjectAddedEvent(context.Background(), + &project.NewAggregate("project1", "org1").Aggregate, + "projectname1", + ), + ), + eventFromEventPusher( + project.NewRoleAddedEvent(context.Background(), + &project.NewAggregate("project1", "org1").Aggregate, + "rolekey1", + "rolekey", + "", + ), + ), + eventFromEventPusher( + project.NewRoleAddedEvent(context.Background(), + &project.NewAggregate("project1", "org1").Aggregate, + "rolekey2", + "rolekey 2", + "", + ), + ), + ), + expectFilter( + eventFromEventPusher( + usergrant.NewUserGrantAddedEvent(context.Background(), + &usergrant.NewAggregate("usergrant1", "org1").Aggregate, + "user1", + "project1", + "", []string{"rolekey1"}), + ), + ), + expectPush( + []*repository.Event{ + eventFromEventPusher(usergrant.NewUserGrantChangedEvent(context.Background(), + &usergrant.NewAggregate("usergrant1", "org1").Aggregate, + []string{"rolekey1", "rolekey2"}, + )), + }, + ), + ), + }, + args: args{ + ctx: authz.NewMockContextWithPermissions("", "", []string{domain.RoleProjectOwner}), + userGrant: &domain.UserGrant{ + ObjectRoot: models.ObjectRoot{ + AggregateID: "usergrant1", + }, + UserID: "user1", + ProjectID: "project1", + RoleKeys: []string{"rolekey1", "rolekey2"}, + }, + resourceOwner: "org1", + }, + res: res{ + want: &domain.UserGrant{ + ObjectRoot: models.ObjectRoot{ + AggregateID: "usergrant1", + ResourceOwner: "org1", + }, + UserID: "user1", + ProjectID: "project1", + RoleKeys: []string{"rolekey1", "rolekey2"}, + State: domain.UserGrantStateActive, + }, + }, + }, + { + name: "usergrant for projectgrant, ok", + fields: fields{ + eventstore: eventstoreExpect( + t, + expectFilter( + eventFromEventPusher( + user.NewHumanAddedEvent(context.Background(), + &user.NewAggregate("user1", "org1").Aggregate, + "username1", + "firstname1", + "lastname1", + "nickname1", + "displayname1", + language.German, + domain.GenderMale, + "email1", + true, + ), + ), + eventFromEventPusher( + project.NewProjectAddedEvent(context.Background(), + &project.NewAggregate("project1", "org1").Aggregate, + "projectname1", + ), + ), + eventFromEventPusher( + project.NewRoleAddedEvent(context.Background(), + &project.NewAggregate("project1", "org1").Aggregate, + "rolekey1", + "rolekey", + "", + ), + ), + eventFromEventPusher( + project.NewRoleAddedEvent(context.Background(), + &project.NewAggregate("project1", "org1").Aggregate, + "rolekey2", + "rolekey2", + "", + ), + ), + eventFromEventPusher( + project.NewGrantAddedEvent(context.Background(), + &project.NewAggregate("project1", "org1").Aggregate, + "projectgrant1", + "org1", + []string{"rolekey1", "rolekey2"}, + ), + ), + ), + expectFilter( + eventFromEventPusher( + usergrant.NewUserGrantAddedEvent(context.Background(), + &usergrant.NewAggregate("usergrant1", "org1").Aggregate, + "user1", + "project1", + "projectgrant1", []string{"rolekey1"}), + ), + ), + expectPush( + []*repository.Event{ + eventFromEventPusher(usergrant.NewUserGrantChangedEvent(context.Background(), + &usergrant.NewAggregate("usergrant1", "org1").Aggregate, + []string{"rolekey1", "rolekey2"}, + )), + }, + ), + ), + }, + args: args{ + ctx: authz.NewMockContextWithPermissions("", "", []string{domain.RoleProjectOwner}), + userGrant: &domain.UserGrant{ + ObjectRoot: models.ObjectRoot{ + AggregateID: "usergrant1", + }, + UserID: "user1", + ProjectID: "project1", + ProjectGrantID: "projectgrant1", + RoleKeys: []string{"rolekey1", "rolekey2"}, + }, + resourceOwner: "org1", + }, + res: res{ + want: &domain.UserGrant{ + ObjectRoot: models.ObjectRoot{ + AggregateID: "usergrant1", + ResourceOwner: "org1", + }, + UserID: "user1", + ProjectID: "project1", + ProjectGrantID: "projectgrant1", + RoleKeys: []string{"rolekey1", "rolekey2"}, + State: domain.UserGrantStateActive, + }, + }, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + r := &Commands{ + eventstore: tt.fields.eventstore, + } + got, err := r.ChangeUserGrant(tt.args.ctx, tt.args.userGrant, tt.args.resourceOwner) + 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_DeactivateUserGrant(t *testing.T) { + type fields struct { + eventstore *eventstore.Eventstore + } + type args struct { + ctx context.Context + userGrantID 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 usergrantID, error", + fields: fields{ + eventstore: eventstoreExpect( + t, + ), + }, + args: args{ + ctx: context.Background(), + resourceOwner: "org1", + }, + res: res{ + err: caos_errs.IsErrorInvalidArgument, + }, + }, + { + name: "invalid resourceOwner, error", + fields: fields{ + eventstore: eventstoreExpect( + t, + ), + }, + args: args{ + ctx: context.Background(), + userGrantID: "usergrant1", + }, + res: res{ + err: caos_errs.IsErrorInvalidArgument, + }, + }, + { + name: "usergrant not existing, not found error", + fields: fields{ + eventstore: eventstoreExpect( + t, + expectFilter(), + ), + }, + args: args{ + ctx: authz.NewMockContextWithPermissions("", "", []string{domain.RoleProjectOwner}), + userGrantID: "usergrant1", + resourceOwner: "org1", + }, + res: res{ + err: caos_errs.IsNotFound, + }, + }, + { + name: "usergrant removed, not found error", + fields: fields{ + eventstore: eventstoreExpect( + t, + expectFilter( + eventFromEventPusher( + usergrant.NewUserGrantAddedEvent(context.Background(), + &usergrant.NewAggregate("usergrant1", "org").Aggregate, + "user1", + "project1", + "", []string{"rolekey1"}), + ), + eventFromEventPusher( + usergrant.NewUserGrantRemovedEvent(context.Background(), + &usergrant.NewAggregate("usergrant1", "org").Aggregate, + "user1", + "project1", + ""), + ), + ), + ), + }, + args: args{ + ctx: authz.NewMockContextWithPermissions("", "", []string{domain.RoleProjectOwner}), + userGrantID: "usergrant1", + resourceOwner: "org1", + }, + res: res{ + err: caos_errs.IsNotFound, + }, + }, + { + name: "no permissions, permisison denied error", + fields: fields{ + eventstore: eventstoreExpect( + t, + expectFilter( + eventFromEventPusher( + usergrant.NewUserGrantAddedEvent(context.Background(), + &usergrant.NewAggregate("usergrant1", "org").Aggregate, + "user1", + "project1", + "", []string{"rolekey1"}), + ), + ), + ), + }, + args: args{ + ctx: context.Background(), + userGrantID: "usergrant1", + resourceOwner: "org1", + }, + res: res{ + err: caos_errs.IsPermissionDenied, + }, + }, + { + name: "already deactivated, precondition error", + fields: fields{ + eventstore: eventstoreExpect( + t, + expectFilter( + eventFromEventPusher( + usergrant.NewUserGrantAddedEvent(context.Background(), + &usergrant.NewAggregate("usergrant1", "org").Aggregate, + "user1", + "project1", + "", []string{"rolekey1"}), + ), + eventFromEventPusher( + usergrant.NewUserGrantDeactivatedEvent(context.Background(), + &usergrant.NewAggregate("usergrant1", "org").Aggregate), + ), + ), + ), + }, + args: args{ + ctx: context.Background(), + userGrantID: "usergrant1", + resourceOwner: "org1", + }, + res: res{ + err: caos_errs.IsPreconditionFailed, + }, + }, + { + name: "deactivated, ok", + fields: fields{ + eventstore: eventstoreExpect( + t, + expectFilter( + eventFromEventPusher( + usergrant.NewUserGrantAddedEvent(context.Background(), + &usergrant.NewAggregate("usergrant1", "org1").Aggregate, + "user1", + "project1", + "", []string{"rolekey1"}), + ), + ), + expectPush( + []*repository.Event{ + eventFromEventPusher( + usergrant.NewUserGrantDeactivatedEvent(context.Background(), + &usergrant.NewAggregate("usergrant1", "org1").Aggregate, + ), + ), + }, + ), + ), + }, + args: args{ + ctx: authz.NewMockContextWithPermissions("", "", []string{domain.RoleProjectOwner}), + userGrantID: "usergrant1", + resourceOwner: "org1", + }, + res: res{ + want: &domain.ObjectDetails{ + ResourceOwner: "org1", + }, + }, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + r := &Commands{ + eventstore: tt.fields.eventstore, + } + got, err := r.DeactivateUserGrant(tt.args.ctx, tt.args.userGrantID, tt.args.resourceOwner) + 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_ReactivateUserGrant(t *testing.T) { + type fields struct { + eventstore *eventstore.Eventstore + } + type args struct { + ctx context.Context + userGrantID 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 usergrantID, error", + fields: fields{ + eventstore: eventstoreExpect( + t, + ), + }, + args: args{ + ctx: context.Background(), + resourceOwner: "org1", + }, + res: res{ + err: caos_errs.IsErrorInvalidArgument, + }, + }, + { + name: "invalid resourceOwner, error", + fields: fields{ + eventstore: eventstoreExpect( + t, + ), + }, + args: args{ + ctx: context.Background(), + userGrantID: "usergrant1", + }, + res: res{ + err: caos_errs.IsErrorInvalidArgument, + }, + }, + { + name: "usergrant not existing, not found error", + fields: fields{ + eventstore: eventstoreExpect( + t, + expectFilter(), + ), + }, + args: args{ + ctx: authz.NewMockContextWithPermissions("", "", []string{domain.RoleProjectOwner}), + userGrantID: "usergrant1", + resourceOwner: "org1", + }, + res: res{ + err: caos_errs.IsNotFound, + }, + }, + { + name: "usergrant removed, not found error", + fields: fields{ + eventstore: eventstoreExpect( + t, + expectFilter( + eventFromEventPusher( + usergrant.NewUserGrantAddedEvent(context.Background(), + &usergrant.NewAggregate("usergrant1", "org").Aggregate, + "user1", + "project1", + "", []string{"rolekey1"}), + ), + eventFromEventPusher( + usergrant.NewUserGrantRemovedEvent(context.Background(), + &usergrant.NewAggregate("usergrant1", "org").Aggregate, + "user1", + "project1", + ""), + ), + ), + ), + }, + args: args{ + ctx: authz.NewMockContextWithPermissions("", "", []string{domain.RoleProjectOwner}), + userGrantID: "usergrant1", + resourceOwner: "org1", + }, + res: res{ + err: caos_errs.IsNotFound, + }, + }, + { + name: "no permissions, permisison denied error", + fields: fields{ + eventstore: eventstoreExpect( + t, + expectFilter( + eventFromEventPusher( + usergrant.NewUserGrantAddedEvent(context.Background(), + &usergrant.NewAggregate("usergrant1", "org").Aggregate, + "user1", + "project1", + "", []string{"rolekey1"}), + ), + eventFromEventPusher( + usergrant.NewUserGrantDeactivatedEvent(context.Background(), + &usergrant.NewAggregate("usergrant1", "org").Aggregate), + ), + ), + ), + }, + args: args{ + ctx: context.Background(), + userGrantID: "usergrant1", + resourceOwner: "org1", + }, + res: res{ + err: caos_errs.IsPermissionDenied, + }, + }, + { + name: "already active, precondition error", + fields: fields{ + eventstore: eventstoreExpect( + t, + expectFilter( + eventFromEventPusher( + usergrant.NewUserGrantAddedEvent(context.Background(), + &usergrant.NewAggregate("usergrant1", "org").Aggregate, + "user1", + "project1", + "", []string{"rolekey1"}), + ), + ), + ), + }, + args: args{ + ctx: context.Background(), + userGrantID: "usergrant1", + resourceOwner: "org1", + }, + res: res{ + err: caos_errs.IsPreconditionFailed, + }, + }, + { + name: "reactivated, ok", + fields: fields{ + eventstore: eventstoreExpect( + t, + expectFilter( + eventFromEventPusher( + usergrant.NewUserGrantAddedEvent(context.Background(), + &usergrant.NewAggregate("usergrant1", "org1").Aggregate, + "user1", + "project1", + "", []string{"rolekey1"}), + ), + eventFromEventPusher( + usergrant.NewUserGrantDeactivatedEvent(context.Background(), + &usergrant.NewAggregate("usergrant1", "org").Aggregate), + ), + ), + expectPush( + []*repository.Event{ + eventFromEventPusher( + usergrant.NewUserGrantReactivatedEvent(context.Background(), + &usergrant.NewAggregate("usergrant1", "org1").Aggregate, + ), + ), + }, + ), + ), + }, + args: args{ + ctx: authz.NewMockContextWithPermissions("", "", []string{domain.RoleProjectOwner}), + userGrantID: "usergrant1", + resourceOwner: "org1", + }, + res: res{ + want: &domain.ObjectDetails{ + ResourceOwner: "org1", + }, + }, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + r := &Commands{ + eventstore: tt.fields.eventstore, + } + got, err := r.ReactivateUserGrant(tt.args.ctx, tt.args.userGrantID, tt.args.resourceOwner) + 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_RemoveUserGrant(t *testing.T) { + type fields struct { + eventstore *eventstore.Eventstore + } + type args struct { + ctx context.Context + userGrantID 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 usergrantID, error", + fields: fields{ + eventstore: eventstoreExpect( + t, + ), + }, + args: args{ + ctx: context.Background(), + resourceOwner: "org1", + }, + res: res{ + err: caos_errs.IsErrorInvalidArgument, + }, + }, + { + name: "usergrant not existing, not found error", + fields: fields{ + eventstore: eventstoreExpect( + t, + expectFilter(), + ), + }, + args: args{ + ctx: authz.NewMockContextWithPermissions("", "", []string{domain.RoleProjectOwner}), + userGrantID: "usergrant1", + resourceOwner: "org1", + }, + res: res{ + err: caos_errs.IsNotFound, + }, + }, + { + name: "usergrant removed, not found error", + fields: fields{ + eventstore: eventstoreExpect( + t, + expectFilter( + eventFromEventPusher( + usergrant.NewUserGrantAddedEvent(context.Background(), + &usergrant.NewAggregate("usergrant1", "org").Aggregate, + "user1", + "project1", + "", []string{"rolekey1"}), + ), + eventFromEventPusher( + usergrant.NewUserGrantRemovedEvent(context.Background(), + &usergrant.NewAggregate("usergrant1", "org").Aggregate, + "user1", + "project1", + ""), + ), + ), + ), + }, + args: args{ + ctx: authz.NewMockContextWithPermissions("", "", []string{domain.RoleProjectOwner}), + userGrantID: "usergrant1", + resourceOwner: "org1", + }, + res: res{ + err: caos_errs.IsNotFound, + }, + }, + { + name: "no permissions, permisison denied error", + fields: fields{ + eventstore: eventstoreExpect( + t, + expectFilter( + eventFromEventPusher( + usergrant.NewUserGrantAddedEvent(context.Background(), + &usergrant.NewAggregate("usergrant1", "org").Aggregate, + "user1", + "project1", + "", []string{"rolekey1"}), + ), + eventFromEventPusher( + usergrant.NewUserGrantDeactivatedEvent(context.Background(), + &usergrant.NewAggregate("usergrant1", "org").Aggregate), + ), + ), + ), + }, + args: args{ + ctx: context.Background(), + userGrantID: "usergrant1", + resourceOwner: "org1", + }, + res: res{ + err: caos_errs.IsPermissionDenied, + }, + }, + { + name: "remove usergrant project, ok", + fields: fields{ + eventstore: eventstoreExpect( + t, + expectFilter( + eventFromEventPusher( + usergrant.NewUserGrantAddedEvent(context.Background(), + &usergrant.NewAggregate("usergrant1", "org1").Aggregate, + "user1", + "project1", + "", []string{"rolekey1"}), + ), + ), + expectPush( + []*repository.Event{ + eventFromEventPusher( + usergrant.NewUserGrantRemovedEvent(context.Background(), + &usergrant.NewAggregate("usergrant1", "org1").Aggregate, + "user1", + "project1", + "", + ), + ), + }, + uniqueConstraintsFromEventConstraint(usergrant.NewRemoveUserGrantUniqueConstraint("org1", "user1", "project1", "")), + ), + ), + }, + args: args{ + ctx: authz.NewMockContextWithPermissions("", "", []string{domain.RoleProjectOwner}), + userGrantID: "usergrant1", + resourceOwner: "org1", + }, + res: res{ + want: &domain.ObjectDetails{ + ResourceOwner: "org1", + }, + }, + }, + { + name: "remove usergrant projectgrant, ok", + fields: fields{ + eventstore: eventstoreExpect( + t, + expectFilter( + eventFromEventPusher( + usergrant.NewUserGrantAddedEvent(context.Background(), + &usergrant.NewAggregate("usergrant1", "org1").Aggregate, + "user1", + "project1", + "projectgrant1", []string{"rolekey1"}), + ), + ), + expectPush( + []*repository.Event{ + eventFromEventPusher( + usergrant.NewUserGrantRemovedEvent(context.Background(), + &usergrant.NewAggregate("usergrant1", "org1").Aggregate, + "user1", + "project1", + "projectgrant1", + ), + ), + }, + uniqueConstraintsFromEventConstraint(usergrant.NewRemoveUserGrantUniqueConstraint("org1", "user1", "project1", "projectgrant1")), + ), + ), + }, + args: args{ + ctx: authz.NewMockContextWithPermissions("", "", []string{domain.RoleProjectOwner}), + userGrantID: "usergrant1", + resourceOwner: "org1", + }, + res: res{ + want: &domain.ObjectDetails{ + ResourceOwner: "org1", + }, + }, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + r := &Commands{ + eventstore: tt.fields.eventstore, + } + got, err := r.RemoveUserGrant(tt.args.ctx, tt.args.userGrantID, tt.args.resourceOwner) + 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_BulkRemoveUserGrant(t *testing.T) { + type fields struct { + eventstore *eventstore.Eventstore + } + type args struct { + ctx context.Context + userGrantIDs []string + resourceOwner string + } + type res struct { + want *domain.ObjectDetails + err func(error) bool + } + tests := []struct { + name string + fields fields + args args + res res + }{ + { + name: "empty usergrantid list, error", + fields: fields{ + eventstore: eventstoreExpect( + t, + ), + }, + args: args{ + ctx: context.Background(), + resourceOwner: "org1", + }, + res: res{ + err: caos_errs.IsErrorInvalidArgument, + }, + }, + { + name: "usergrant not existing, not found error", + fields: fields{ + eventstore: eventstoreExpect( + t, + expectFilter(), + ), + }, + args: args{ + ctx: authz.NewMockContextWithPermissions("", "", []string{domain.RoleProjectOwner}), + userGrantIDs: []string{"usergrant1", "usergrant2"}, + resourceOwner: "org1", + }, + res: res{ + err: caos_errs.IsNotFound, + }, + }, + { + name: "usergrant removed, not found error", + fields: fields{ + eventstore: eventstoreExpect( + t, + expectFilter( + eventFromEventPusher( + usergrant.NewUserGrantAddedEvent(context.Background(), + &usergrant.NewAggregate("usergrant1", "org").Aggregate, + "user1", + "project1", + "", []string{"rolekey1"}), + ), + eventFromEventPusher( + usergrant.NewUserGrantRemovedEvent(context.Background(), + &usergrant.NewAggregate("usergrant1", "org").Aggregate, + "user1", + "project1", + ""), + ), + ), + ), + }, + args: args{ + ctx: authz.NewMockContextWithPermissions("", "", []string{domain.RoleProjectOwner}), + userGrantIDs: []string{"usergrant1", "usergrant2"}, + resourceOwner: "org1", + }, + res: res{ + err: caos_errs.IsNotFound, + }, + }, + { + name: "no permissions, permisison denied error", + fields: fields{ + eventstore: eventstoreExpect( + t, + expectFilter( + eventFromEventPusher( + usergrant.NewUserGrantAddedEvent(context.Background(), + &usergrant.NewAggregate("usergrant1", "org").Aggregate, + "user1", + "project1", + "", []string{"rolekey1"}), + ), + ), + ), + }, + args: args{ + ctx: context.Background(), + userGrantIDs: []string{"usergrant1", "usergrant2"}, + resourceOwner: "org1", + }, + res: res{ + err: caos_errs.IsPermissionDenied, + }, + }, + { + name: "remove usergrants project, ok", + fields: fields{ + eventstore: eventstoreExpect( + t, + expectFilter( + eventFromEventPusher( + usergrant.NewUserGrantAddedEvent(context.Background(), + &usergrant.NewAggregate("usergrant1", "org1").Aggregate, + "user1", + "project1", + "", []string{"rolekey1"}), + ), + ), + expectFilter( + eventFromEventPusher( + usergrant.NewUserGrantAddedEvent(context.Background(), + &usergrant.NewAggregate("usergrant2", "org1").Aggregate, + "user2", + "project2", + "", []string{"rolekey1"}), + ), + ), + expectPush( + []*repository.Event{ + eventFromEventPusher( + usergrant.NewUserGrantRemovedEvent(context.Background(), + &usergrant.NewAggregate("usergrant1", "org1").Aggregate, + "user1", + "project1", + "", + ), + ), + eventFromEventPusher( + usergrant.NewUserGrantRemovedEvent(context.Background(), + &usergrant.NewAggregate("usergrant2", "org1").Aggregate, + "user2", + "project2", + "", + ), + ), + }, + uniqueConstraintsFromEventConstraint(usergrant.NewRemoveUserGrantUniqueConstraint("org1", "user1", "project1", "")), + uniqueConstraintsFromEventConstraint(usergrant.NewRemoveUserGrantUniqueConstraint("org1", "user2", "project2", "")), + ), + ), + }, + args: args{ + ctx: authz.NewMockContextWithPermissions("", "", []string{domain.RoleProjectOwner}), + userGrantIDs: []string{"usergrant1", "usergrant2"}, + resourceOwner: "org1", + }, + res: res{}, + }, + { + name: "remove usergrants projectgrant, ok", + fields: fields{ + eventstore: eventstoreExpect( + t, + expectFilter( + eventFromEventPusher( + usergrant.NewUserGrantAddedEvent(context.Background(), + &usergrant.NewAggregate("usergrant1", "org1").Aggregate, + "user1", + "project1", + "projectgrant1", []string{"rolekey1"}), + ), + ), + expectFilter( + eventFromEventPusher( + usergrant.NewUserGrantAddedEvent(context.Background(), + &usergrant.NewAggregate("usergrant2", "org1").Aggregate, + "user2", + "project2", + "projectgrant2", []string{"rolekey1"}), + ), + ), + expectPush( + []*repository.Event{ + eventFromEventPusher( + usergrant.NewUserGrantRemovedEvent(context.Background(), + &usergrant.NewAggregate("usergrant1", "org1").Aggregate, + "user1", + "project1", + "projectgrant1", + ), + ), + eventFromEventPusher( + usergrant.NewUserGrantRemovedEvent(context.Background(), + &usergrant.NewAggregate("usergrant2", "org1").Aggregate, + "user2", + "project2", + "projectgrant2", + ), + ), + }, + uniqueConstraintsFromEventConstraint(usergrant.NewRemoveUserGrantUniqueConstraint("org1", "user1", "project1", "projectgrant1")), + uniqueConstraintsFromEventConstraint(usergrant.NewRemoveUserGrantUniqueConstraint("org1", "user2", "project2", "projectgrant2")), + ), + ), + }, + args: args{ + ctx: authz.NewMockContextWithPermissions("", "", []string{domain.RoleProjectOwner}), + userGrantIDs: []string{"usergrant1", "usergrant2"}, + resourceOwner: "org1", + }, + res: res{}, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + r := &Commands{ + eventstore: tt.fields.eventstore, + } + err := r.BulkRemoveUserGrant(tt.args.ctx, tt.args.userGrantIDs, tt.args.resourceOwner) + 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) + } + }) + } +} diff --git a/internal/domain/application_api.go b/internal/domain/application_api.go index 161a99c17..05af127e8 100644 --- a/internal/domain/application_api.go +++ b/internal/domain/application_api.go @@ -34,7 +34,7 @@ const ( ) func (a *APIApp) IsValid() bool { - return true + return a.AppName != "" } func (a *APIApp) setClientID(clientID string) { diff --git a/internal/domain/application_oidc.go b/internal/domain/application_oidc.go index 5a91092c6..d05db556c 100644 --- a/internal/domain/application_oidc.go +++ b/internal/domain/application_oidc.go @@ -113,6 +113,9 @@ const ( ) func (a *OIDCApp) IsValid() bool { + if a.AppName == "" || a.ClockSkew > time.Second*5 || a.ClockSkew < time.Second*0 { + return false + } grantTypes := a.getRequiredGrantTypes() for _, grantType := range grantTypes { ok := containsOIDCGrantType(a.GrantTypes, grantType) diff --git a/internal/domain/application_oidc_test.go b/internal/domain/application_oidc_test.go new file mode 100644 index 000000000..8d9c8af7a --- /dev/null +++ b/internal/domain/application_oidc_test.go @@ -0,0 +1,185 @@ +package domain + +import ( + "github.com/caos/zitadel/internal/eventstore/v1/models" + "testing" + "time" +) + +func TestApplicationValid(t *testing.T) { + type args struct { + app *OIDCApp + } + tests := []struct { + name string + args args + result bool + }{ + { + name: "no app name", + args: args{ + app: &OIDCApp{ + ObjectRoot: models.ObjectRoot{AggregateID: "AggregateID"}, + AppID: "AppID", + AppName: "", + ResponseTypes: []OIDCResponseType{OIDCResponseTypeCode}, + GrantTypes: []OIDCGrantType{OIDCGrantTypeAuthorizationCode}, + }, + }, + result: false, + }, + { + name: "invalid clock skew", + args: args{ + app: &OIDCApp{ + ObjectRoot: models.ObjectRoot{AggregateID: "AggregateID"}, + AppID: "AppID", + AppName: "AppName", + ClockSkew: time.Minute * 1, + ResponseTypes: []OIDCResponseType{OIDCResponseTypeCode}, + GrantTypes: []OIDCGrantType{OIDCGrantTypeAuthorizationCode}, + }, + }, + result: false, + }, + { + name: "invalid clock skew minus", + args: args{ + app: &OIDCApp{ + ObjectRoot: models.ObjectRoot{AggregateID: "AggregateID"}, + AppID: "AppID", + AppName: "AppName", + ClockSkew: time.Minute * -1, + ResponseTypes: []OIDCResponseType{OIDCResponseTypeCode}, + GrantTypes: []OIDCGrantType{OIDCGrantTypeAuthorizationCode}, + }, + }, + result: false, + }, + { + name: "valid oidc application: responsetype code", + args: args{ + app: &OIDCApp{ + ObjectRoot: models.ObjectRoot{AggregateID: "AggregateID"}, + AppID: "AppID", + AppName: "Name", + ResponseTypes: []OIDCResponseType{OIDCResponseTypeCode}, + GrantTypes: []OIDCGrantType{OIDCGrantTypeAuthorizationCode}, + }, + }, + result: true, + }, + { + name: "invalid oidc application: responsetype code", + args: args{ + app: &OIDCApp{ + ObjectRoot: models.ObjectRoot{AggregateID: "AggregateID"}, + AppID: "AppID", + AppName: "Name", + ResponseTypes: []OIDCResponseType{OIDCResponseTypeCode}, + GrantTypes: []OIDCGrantType{OIDCGrantTypeImplicit}, + }, + }, + result: false, + }, + { + name: "valid oidc application: responsetype id_token", + args: args{ + app: &OIDCApp{ + ObjectRoot: models.ObjectRoot{AggregateID: "AggregateID"}, + AppID: "AppID", + AppName: "Name", + ResponseTypes: []OIDCResponseType{OIDCResponseTypeIDToken}, + GrantTypes: []OIDCGrantType{OIDCGrantTypeImplicit}, + }, + }, + result: true, + }, + { + name: "invalid oidc application: responsetype id_token", + args: args{ + app: &OIDCApp{ + ObjectRoot: models.ObjectRoot{AggregateID: "AggregateID"}, + AppID: "AppID", + AppName: "Name", + ResponseTypes: []OIDCResponseType{OIDCResponseTypeIDToken}, + GrantTypes: []OIDCGrantType{OIDCGrantTypeAuthorizationCode}, + }, + }, + result: false, + }, + { + name: "valid oidc application: responsetype token_id_token", + args: args{ + app: &OIDCApp{ + ObjectRoot: models.ObjectRoot{AggregateID: "AggregateID"}, + AppID: "AppID", + AppName: "Name", + ResponseTypes: []OIDCResponseType{OIDCResponseTypeIDTokenToken}, + GrantTypes: []OIDCGrantType{OIDCGrantTypeImplicit}, + }, + }, + result: true, + }, + { + name: "invalid oidc application: responsetype token_id_token", + args: args{ + app: &OIDCApp{ + ObjectRoot: models.ObjectRoot{AggregateID: "AggregateID"}, + AppID: "AppID", + AppName: "Name", + ResponseTypes: []OIDCResponseType{OIDCResponseTypeIDTokenToken}, + GrantTypes: []OIDCGrantType{OIDCGrantTypeAuthorizationCode}, + }, + }, + result: false, + }, + { + name: "valid oidc application: responsetype code & id_token", + args: args{ + app: &OIDCApp{ + ObjectRoot: models.ObjectRoot{AggregateID: "AggregateID"}, + AppID: "AppID", + AppName: "Name", + ResponseTypes: []OIDCResponseType{OIDCResponseTypeCode, OIDCResponseTypeIDToken}, + GrantTypes: []OIDCGrantType{OIDCGrantTypeAuthorizationCode, OIDCGrantTypeImplicit}, + }, + }, + result: true, + }, + { + name: "valid oidc application: responsetype code & token_id_token", + args: args{ + app: &OIDCApp{ + ObjectRoot: models.ObjectRoot{AggregateID: "AggregateID"}, + AppID: "AppID", + AppName: "Name", + ResponseTypes: []OIDCResponseType{OIDCResponseTypeCode, OIDCResponseTypeIDTokenToken}, + GrantTypes: []OIDCGrantType{OIDCGrantTypeAuthorizationCode, OIDCGrantTypeImplicit}, + }, + }, + result: true, + }, + { + name: "valid oidc application: responsetype code & id_token & token_id_token", + args: args{ + app: &OIDCApp{ + ObjectRoot: models.ObjectRoot{AggregateID: "AggregateID"}, + AppID: "AppID", + AppName: "Name", + ResponseTypes: []OIDCResponseType{OIDCResponseTypeCode, OIDCResponseTypeIDToken, OIDCResponseTypeIDTokenToken}, + GrantTypes: []OIDCGrantType{OIDCGrantTypeAuthorizationCode, OIDCGrantTypeImplicit}, + }, + }, + result: true, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + result := tt.args.app.IsValid() + if result != tt.result { + t.Errorf("got wrong result: expected: %v, actual: %v ", tt.result, result) + } + }) + } +} diff --git a/internal/domain/member.go b/internal/domain/member.go index 54e954e40..8b257fb34 100644 --- a/internal/domain/member.go +++ b/internal/domain/member.go @@ -25,6 +25,10 @@ func (i *Member) IsValid() bool { return i.AggregateID != "" && i.UserID != "" && len(i.Roles) != 0 } +func (i *Member) IsIAMValid() bool { + return i.UserID != "" && len(i.Roles) != 0 +} + type MemberState int32 const ( diff --git a/internal/domain/policy_label.go b/internal/domain/policy_label.go index 591d6d37d..fd174e8fb 100644 --- a/internal/domain/policy_label.go +++ b/internal/domain/policy_label.go @@ -11,3 +11,7 @@ type LabelPolicy struct { PrimaryColor string SecondaryColor string } + +func (p *LabelPolicy) IsValid() bool { + return p.PrimaryColor != "" && p.SecondaryColor != "" +} diff --git a/internal/domain/project.go b/internal/domain/project.go index 5e35fc786..ec83818f5 100644 --- a/internal/domain/project.go +++ b/internal/domain/project.go @@ -7,12 +7,8 @@ import ( type Project struct { models.ObjectRoot - State ProjectState - Name string - Members []*Member - Roles []*ProjectRole - //Applications []*Application - //Grants []*ProjectGrant + State ProjectState + Name string ProjectRoleAssertion bool ProjectRoleCheck bool } diff --git a/internal/domain/project_grant.go b/internal/domain/project_grant.go index 515f1e88a..8721c7d15 100644 --- a/internal/domain/project_grant.go +++ b/internal/domain/project_grant.go @@ -29,21 +29,21 @@ func (p *ProjectGrant) IsValid() bool { return p.GrantedOrgID != "" } -func GetRemovedRoles(existingRoles, newRoles []string) []string { - removed := make([]string, 0) - for _, role := range existingRoles { - if !containsKey(newRoles, role) { - removed = append(removed, role) - } - } - return removed -} - -func containsKey(roles []string, key string) bool { - for _, role := range roles { - if role == key { +func (g *ProjectGrant) HasInvalidRoles(validRoles []string) bool { + for _, roleKey := range g.RoleKeys { + if !containsRoleKey(roleKey, validRoles) { return true } } return false } + +func GetRemovedRoles(existingRoles, newRoles []string) []string { + removed := make([]string, 0) + for _, role := range existingRoles { + if !containsRoleKey(role, newRoles) { + removed = append(removed, role) + } + } + return removed +} diff --git a/internal/domain/project_role.go b/internal/domain/project_role.go index 1d8b6adff..574187c45 100644 --- a/internal/domain/project_role.go +++ b/internal/domain/project_role.go @@ -27,3 +27,12 @@ func NewProjectRole(projectID, key string) *ProjectRole { func (p *ProjectRole) IsValid() bool { return p.AggregateID != "" && p.Key != "" } + +func containsRoleKey(roleKey string, validRoles []string) bool { + for _, validRole := range validRoles { + if roleKey == validRole { + return true + } + } + return false +} diff --git a/internal/domain/user_grant.go b/internal/domain/user_grant.go index 59f84ea0a..f673780e8 100644 --- a/internal/domain/user_grant.go +++ b/internal/domain/user_grant.go @@ -24,3 +24,12 @@ const ( func (u *UserGrant) IsValid() bool { return u.ProjectID != "" && u.UserID != "" } + +func (g *UserGrant) HasInvalidRoles(validRoles []string) bool { + for _, roleKey := range g.RoleKeys { + if !containsRoleKey(roleKey, validRoles) { + return true + } + } + return false +} diff --git a/internal/eventstore/eventstore.go b/internal/eventstore/eventstore.go index 00f81c3c7..ca1ec312b 100644 --- a/internal/eventstore/eventstore.go +++ b/internal/eventstore/eventstore.go @@ -60,7 +60,7 @@ func (es *Eventstore) PushEvents(ctx context.Context, pushEvents ...EventPusher) func eventsToRepository(pushEvents []EventPusher) (events []*repository.Event, constraints []*repository.UniqueConstraint, err error) { events = make([]*repository.Event, len(pushEvents)) for i, event := range pushEvents { - data, err := eventData(event) + data, err := EventData(event) if err != nil { return nil, nil, err } @@ -195,7 +195,7 @@ func (es *Eventstore) RegisterFilterEventMapper(eventType EventType, mapper func return es } -func eventData(event EventPusher) ([]byte, error) { +func EventData(event EventPusher) ([]byte, error) { switch data := event.Data().(type) { case nil: return nil, nil diff --git a/internal/eventstore/eventstore_test.go b/internal/eventstore/eventstore_test.go index cb11c3874..59437de2f 100644 --- a/internal/eventstore/eventstore_test.go +++ b/internal/eventstore/eventstore_test.go @@ -350,13 +350,13 @@ func Test_eventData(t *testing.T) { } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - got, err := eventData(tt.args.event) + got, err := EventData(tt.args.event) if (err != nil) != tt.res.wantErr { - t.Errorf("eventData() error = %v, wantErr %v", err, tt.res.wantErr) + t.Errorf("EventData() error = %v, wantErr %v", err, tt.res.wantErr) return } if !reflect.DeepEqual(got, tt.res.jsonText) { - t.Errorf("eventData() = %v, want %v", string(got), string(tt.res.jsonText)) + t.Errorf("EventData() = %v, want %v", string(got), string(tt.res.jsonText)) } }) } diff --git a/internal/eventstore/repository/mock/gen_mock.go b/internal/eventstore/repository/mock/gen_mock.go new file mode 100644 index 000000000..3685ca779 --- /dev/null +++ b/internal/eventstore/repository/mock/gen_mock.go @@ -0,0 +1,3 @@ +package mock + +//go:generate mockgen -package mock -destination ./repository.mock.go github.com/caos/zitadel/internal/eventstore/repository Repository diff --git a/internal/eventstore/repository/mock/repository.mock.go b/internal/eventstore/repository/mock/repository.mock.go new file mode 100644 index 000000000..70f3d1283 --- /dev/null +++ b/internal/eventstore/repository/mock/repository.mock.go @@ -0,0 +1,98 @@ +// Code generated by MockGen. DO NOT EDIT. +// Source: github.com/caos/zitadel/internal/eventstore/repository (interfaces: Repository) + +// Package mock is a generated GoMock package. +package mock + +import ( + context "context" + repository "github.com/caos/zitadel/internal/eventstore/repository" + gomock "github.com/golang/mock/gomock" + reflect "reflect" +) + +// MockRepository is a mock of Repository interface +type MockRepository struct { + ctrl *gomock.Controller + recorder *MockRepositoryMockRecorder +} + +// MockRepositoryMockRecorder is the mock recorder for MockRepository +type MockRepositoryMockRecorder struct { + mock *MockRepository +} + +// NewMockRepository creates a new mock instance +func NewMockRepository(ctrl *gomock.Controller) *MockRepository { + mock := &MockRepository{ctrl: ctrl} + mock.recorder = &MockRepositoryMockRecorder{mock} + return mock +} + +// EXPECT returns an object that allows the caller to indicate expected use +func (m *MockRepository) EXPECT() *MockRepositoryMockRecorder { + return m.recorder +} + +// Filter mocks base method +func (m *MockRepository) Filter(arg0 context.Context, arg1 *repository.SearchQuery) ([]*repository.Event, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "Filter", arg0, arg1) + ret0, _ := ret[0].([]*repository.Event) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// Filter indicates an expected call of Filter +func (mr *MockRepositoryMockRecorder) Filter(arg0, arg1 interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Filter", reflect.TypeOf((*MockRepository)(nil).Filter), arg0, arg1) +} + +// Health mocks base method +func (m *MockRepository) Health(arg0 context.Context) error { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "Health", arg0) + ret0, _ := ret[0].(error) + return ret0 +} + +// Health indicates an expected call of Health +func (mr *MockRepositoryMockRecorder) Health(arg0 interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Health", reflect.TypeOf((*MockRepository)(nil).Health), arg0) +} + +// LatestSequence mocks base method +func (m *MockRepository) LatestSequence(arg0 context.Context, arg1 *repository.SearchQuery) (uint64, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "LatestSequence", arg0, arg1) + ret0, _ := ret[0].(uint64) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// LatestSequence indicates an expected call of LatestSequence +func (mr *MockRepositoryMockRecorder) LatestSequence(arg0, arg1 interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "LatestSequence", reflect.TypeOf((*MockRepository)(nil).LatestSequence), arg0, arg1) +} + +// Push mocks base method +func (m *MockRepository) Push(arg0 context.Context, arg1 []*repository.Event, arg2 ...*repository.UniqueConstraint) error { + m.ctrl.T.Helper() + varargs := []interface{}{arg0, arg1} + for _, a := range arg2 { + varargs = append(varargs, a) + } + ret := m.ctrl.Call(m, "Push", varargs...) + ret0, _ := ret[0].(error) + return ret0 +} + +// Push indicates an expected call of Push +func (mr *MockRepositoryMockRecorder) Push(arg0, arg1 interface{}, arg2 ...interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + varargs := append([]interface{}{arg0, arg1}, arg2...) + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Push", reflect.TypeOf((*MockRepository)(nil).Push), varargs...) +} diff --git a/internal/eventstore/repository/mock/repository.mock.impl.go b/internal/eventstore/repository/mock/repository.mock.impl.go new file mode 100644 index 000000000..b69894450 --- /dev/null +++ b/internal/eventstore/repository/mock/repository.mock.impl.go @@ -0,0 +1,53 @@ +package mock + +import ( + "context" + "testing" + + "github.com/golang/mock/gomock" + "github.com/stretchr/testify/assert" + + "github.com/caos/zitadel/internal/eventstore/repository" +) + +func NewRepo(t *testing.T) *MockRepository { + return NewMockRepository(gomock.NewController(t)) +} + +func (m *MockRepository) ExpectFilterNoEventsNoError() *MockRepository { + m.EXPECT().Filter(gomock.Any(), gomock.Any()).Return(nil, nil) + return m +} + +func (m *MockRepository) ExpectFilterEvents(events ...*repository.Event) *MockRepository { + m.EXPECT().Filter(gomock.Any(), gomock.Any()).Return(events, nil) + return m +} + +func (m *MockRepository) ExpectPush(expectedEvents []*repository.Event, expectedUniqueConstraints ...*repository.UniqueConstraint) *MockRepository { + m.EXPECT().Push(gomock.Any(), gomock.Any(), gomock.Any()).DoAndReturn( + func(ctx context.Context, events []*repository.Event, uniqueConstraints ...*repository.UniqueConstraint) error { + assert.Equal(m.ctrl.T, expectedEvents, events) + if expectedUniqueConstraints == nil { + expectedUniqueConstraints = []*repository.UniqueConstraint{} + } + assert.Equal(m.ctrl.T, expectedUniqueConstraints, uniqueConstraints) + return nil + }, + ) + return m +} + +func (m *MockRepository) ExpectPushFailed(err error, expectedEvents []*repository.Event, expectedUniqueConstraints ...*repository.UniqueConstraint) *MockRepository { + m.EXPECT().Push(gomock.Any(), gomock.Any(), gomock.Any()).DoAndReturn( + func(ctx context.Context, events []*repository.Event, uniqueConstraints ...*repository.UniqueConstraint) error { + assert.Equal(m.ctrl.T, expectedEvents, events) + if expectedUniqueConstraints == nil { + expectedUniqueConstraints = []*repository.UniqueConstraint{} + } + assert.Equal(m.ctrl.T, expectedUniqueConstraints, uniqueConstraints) + return err + }, + ) + return m +} diff --git a/internal/id/mock/generator.mock.impl.go b/internal/id/mock/generator.mock.impl.go new file mode 100644 index 000000000..e61f80398 --- /dev/null +++ b/internal/id/mock/generator.mock.impl.go @@ -0,0 +1,27 @@ +package mock + +import ( + "testing" + + "github.com/golang/mock/gomock" +) + +func NewIDGenerator(t *testing.T) *MockGenerator { + m := NewMockGenerator(gomock.NewController(t)) + m.EXPECT().Next().Return("1", nil) + return m +} + +func NewIDGeneratorExpectIDs(t *testing.T, ids ...string) *MockGenerator { + m := NewMockGenerator(gomock.NewController(t)) + for _, id := range ids { + m.EXPECT().Next().Return(id, nil) + } + return m +} + +func ExpectID(t *testing.T, id string) *MockGenerator { + m := NewMockGenerator(gomock.NewController(t)) + m.EXPECT().Next().Return(id, nil) + return m +} diff --git a/internal/repository/iam/aggregate.go b/internal/repository/iam/aggregate.go index e979aa55c..f67c98cab 100644 --- a/internal/repository/iam/aggregate.go +++ b/internal/repository/iam/aggregate.go @@ -1,6 +1,7 @@ package iam import ( + "github.com/caos/zitadel/internal/domain" "github.com/caos/zitadel/internal/eventstore" ) @@ -16,3 +17,14 @@ const ( type Aggregate struct { eventstore.Aggregate } + +func NewAggregate() *Aggregate { + return &Aggregate{ + Aggregate: eventstore.Aggregate{ + Typ: AggregateType, + Version: AggregateVersion, + ID: domain.IAMID, + ResourceOwner: domain.IAMID, + }, + } +} diff --git a/internal/repository/iam/eventstore.go b/internal/repository/iam/eventstore.go index e55e77360..44f4d86d8 100644 --- a/internal/repository/iam/eventstore.go +++ b/internal/repository/iam/eventstore.go @@ -15,6 +15,7 @@ func RegisterEventMappers(es *eventstore.Eventstore) { RegisterFilterEventMapper(LoginPolicyAddedEventType, LoginPolicyAddedEventMapper). RegisterFilterEventMapper(LoginPolicyChangedEventType, LoginPolicyChangedEventMapper). RegisterFilterEventMapper(OrgIAMPolicyAddedEventType, OrgIAMPolicyAddedEventMapper). + RegisterFilterEventMapper(OrgIAMPolicyChangedEventType, OrgIAMPolicyChangedEventMapper). RegisterFilterEventMapper(PasswordAgePolicyAddedEventType, PasswordAgePolicyAddedEventMapper). RegisterFilterEventMapper(PasswordAgePolicyChangedEventType, PasswordAgePolicyChangedEventMapper). RegisterFilterEventMapper(PasswordComplexityPolicyAddedEventType, PasswordComplexityPolicyAddedEventMapper). diff --git a/internal/repository/org/aggregate.go b/internal/repository/org/aggregate.go index a2b216c8e..ea8eb7379 100644 --- a/internal/repository/org/aggregate.go +++ b/internal/repository/org/aggregate.go @@ -16,3 +16,14 @@ const ( type Aggregate struct { eventstore.Aggregate } + +func NewAggregate(id, resourceOwner string) *Aggregate { + return &Aggregate{ + Aggregate: eventstore.Aggregate{ + Typ: AggregateType, + Version: AggregateVersion, + ID: id, + ResourceOwner: resourceOwner, + }, + } +} diff --git a/internal/repository/project/aggregate.go b/internal/repository/project/aggregate.go index 441c15ab4..9565656c1 100644 --- a/internal/repository/project/aggregate.go +++ b/internal/repository/project/aggregate.go @@ -12,3 +12,14 @@ const ( type Aggregate struct { eventstore.Aggregate } + +func NewAggregate(id, resourceOwner string) *Aggregate { + return &Aggregate{ + Aggregate: eventstore.Aggregate{ + Typ: AggregateType, + Version: AggregateVersion, + ID: id, + ResourceOwner: resourceOwner, + }, + } +} diff --git a/internal/repository/project/application.go b/internal/repository/project/application.go index 45bac6db5..1111feb85 100644 --- a/internal/repository/project/application.go +++ b/internal/repository/project/application.go @@ -36,9 +36,8 @@ func NewRemoveApplicationUniqueConstraint(name, projectID string) *eventstore.Ev type ApplicationAddedEvent struct { eventstore.BaseEvent `json:"-"` - AppID string `json:"appId,omitempty"` - Name string `json:"name,omitempty"` - projectID string + AppID string `json:"appId,omitempty"` + Name string `json:"name,omitempty"` } func (e *ApplicationAddedEvent) Data() interface{} { @@ -46,15 +45,14 @@ func (e *ApplicationAddedEvent) Data() interface{} { } func (e *ApplicationAddedEvent) UniqueConstraints() []*eventstore.EventUniqueConstraint { - return []*eventstore.EventUniqueConstraint{NewAddApplicationUniqueConstraint(e.Name, e.projectID)} + return []*eventstore.EventUniqueConstraint{NewAddApplicationUniqueConstraint(e.Name, e.Aggregate().ID)} } func NewApplicationAddedEvent( ctx context.Context, aggregate *eventstore.Aggregate, appID, - name, - projectID string, + name string, ) *ApplicationAddedEvent { return &ApplicationAddedEvent{ BaseEvent: *eventstore.NewBaseEventForPush( @@ -62,9 +60,8 @@ func NewApplicationAddedEvent( aggregate, ApplicationAddedType, ), - AppID: appID, - Name: name, - projectID: projectID, + AppID: appID, + Name: name, } } @@ -84,10 +81,9 @@ func ApplicationAddedEventMapper(event *repository.Event) (eventstore.EventReade type ApplicationChangedEvent struct { eventstore.BaseEvent `json:"-"` - AppID string `json:"appId,omitempty"` - Name string `json:"name,omitempty"` - oldName string - projectID string + AppID string `json:"appId,omitempty"` + Name string `json:"name,omitempty"` + oldName string } func (e *ApplicationChangedEvent) Data() interface{} { @@ -96,8 +92,8 @@ func (e *ApplicationChangedEvent) Data() interface{} { func (e *ApplicationChangedEvent) UniqueConstraints() []*eventstore.EventUniqueConstraint { return []*eventstore.EventUniqueConstraint{ - NewRemoveApplicationUniqueConstraint(e.oldName, e.projectID), - NewAddApplicationUniqueConstraint(e.Name, e.projectID), + NewRemoveApplicationUniqueConstraint(e.oldName, e.Aggregate().ID), + NewAddApplicationUniqueConstraint(e.Name, e.Aggregate().ID), } } @@ -106,8 +102,7 @@ func NewApplicationChangedEvent( aggregate *eventstore.Aggregate, appID, oldName, - newName, - projectID string, + newName string, ) *ApplicationChangedEvent { return &ApplicationChangedEvent{ BaseEvent: *eventstore.NewBaseEventForPush( @@ -115,10 +110,9 @@ func NewApplicationChangedEvent( aggregate, ApplicationChangedType, ), - AppID: appID, - Name: newName, - oldName: oldName, - projectID: projectID, + AppID: appID, + Name: newName, + oldName: oldName, } } @@ -222,9 +216,8 @@ func ApplicationReactivatedEventMapper(event *repository.Event) (eventstore.Even type ApplicationRemovedEvent struct { eventstore.BaseEvent `json:"-"` - AppID string `json:"appId,omitempty"` - name string - projectID string + AppID string `json:"appId,omitempty"` + name string } func (e *ApplicationRemovedEvent) Data() interface{} { @@ -232,15 +225,14 @@ func (e *ApplicationRemovedEvent) Data() interface{} { } func (e *ApplicationRemovedEvent) UniqueConstraints() []*eventstore.EventUniqueConstraint { - return []*eventstore.EventUniqueConstraint{NewRemoveApplicationUniqueConstraint(e.name, e.projectID)} + return []*eventstore.EventUniqueConstraint{NewRemoveApplicationUniqueConstraint(e.name, e.Aggregate().ID)} } func NewApplicationRemovedEvent( ctx context.Context, aggregate *eventstore.Aggregate, appID, - name, - projectID string, + name string, ) *ApplicationRemovedEvent { return &ApplicationRemovedEvent{ BaseEvent: *eventstore.NewBaseEventForPush( @@ -248,9 +240,8 @@ func NewApplicationRemovedEvent( aggregate, ApplicationRemovedType, ), - AppID: appID, - name: name, - projectID: projectID, + AppID: appID, + name: name, } } diff --git a/internal/repository/project/grant.go b/internal/repository/project/grant.go index 48892289c..41fa983bc 100644 --- a/internal/repository/project/grant.go +++ b/internal/repository/project/grant.go @@ -40,7 +40,6 @@ type GrantAddedEvent struct { GrantID string `json:"grantId,omitempty"` GrantedOrgID string `json:"grantedOrgId,omitempty"` RoleKeys []string `json:"roleKeys,omitempty"` - projectID string } func (e *GrantAddedEvent) Data() interface{} { @@ -48,15 +47,14 @@ func (e *GrantAddedEvent) Data() interface{} { } func (e *GrantAddedEvent) UniqueConstraints() []*eventstore.EventUniqueConstraint { - return []*eventstore.EventUniqueConstraint{NewAddProjectGrantUniqueConstraint(e.GrantedOrgID, e.projectID)} + return []*eventstore.EventUniqueConstraint{NewAddProjectGrantUniqueConstraint(e.GrantedOrgID, e.Aggregate().ID)} } func NewGrantAddedEvent( ctx context.Context, aggregate *eventstore.Aggregate, grantID, - grantedOrgID, - projectID string, + grantedOrgID string, roleKeys []string, ) *GrantAddedEvent { return &GrantAddedEvent{ @@ -68,7 +66,6 @@ func NewGrantAddedEvent( GrantID: grantID, GrantedOrgID: grantedOrgID, RoleKeys: roleKeys, - projectID: projectID, } } @@ -264,7 +261,6 @@ type GrantRemovedEvent struct { GrantID string `json:"grantId,omitempty"` grantedOrgID string - projectID string } func (e *GrantRemovedEvent) Data() interface{} { @@ -272,15 +268,14 @@ func (e *GrantRemovedEvent) Data() interface{} { } func (e *GrantRemovedEvent) UniqueConstraints() []*eventstore.EventUniqueConstraint { - return []*eventstore.EventUniqueConstraint{NewRemoveProjectGrantUniqueConstraint(e.grantedOrgID, e.projectID)} + return []*eventstore.EventUniqueConstraint{NewRemoveProjectGrantUniqueConstraint(e.grantedOrgID, e.Aggregate().ID)} } func NewGrantRemovedEvent( ctx context.Context, aggregate *eventstore.Aggregate, grantID, - grantedOrgID, - projectID string, + grantedOrgID string, ) *GrantRemovedEvent { return &GrantRemovedEvent{ BaseEvent: *eventstore.NewBaseEventForPush( @@ -289,7 +284,6 @@ func NewGrantRemovedEvent( GrantRemovedType, ), GrantID: grantID, - projectID: projectID, grantedOrgID: grantedOrgID, } } diff --git a/internal/repository/project/grant_member.go b/internal/repository/project/grant_member.go index 67beafd76..30582d2fd 100644 --- a/internal/repository/project/grant_member.go +++ b/internal/repository/project/grant_member.go @@ -35,10 +35,9 @@ func NewRemoveProjectGrantMemberUniqueConstraint(projectID, userID, grantID stri type GrantMemberAddedEvent struct { eventstore.BaseEvent `json:"-"` - Roles []string `json:"roles"` - UserID string `json:"userId"` - GrantID string `json:"grantId"` - projectID string + Roles []string `json:"roles"` + UserID string `json:"userId"` + GrantID string `json:"grantId"` } func (e *GrantMemberAddedEvent) Data() interface{} { @@ -46,13 +45,12 @@ func (e *GrantMemberAddedEvent) Data() interface{} { } func (e *GrantMemberAddedEvent) UniqueConstraints() []*eventstore.EventUniqueConstraint { - return []*eventstore.EventUniqueConstraint{NewAddProjectGrantMemberUniqueConstraint(e.projectID, e.UserID, e.GrantID)} + return []*eventstore.EventUniqueConstraint{NewAddProjectGrantMemberUniqueConstraint(e.Aggregate().ID, e.UserID, e.GrantID)} } func NewProjectGrantMemberAddedEvent( ctx context.Context, aggregate *eventstore.Aggregate, - projectID, userID, grantID string, roles ...string, @@ -63,10 +61,9 @@ func NewProjectGrantMemberAddedEvent( aggregate, GrantMemberAddedType, ), - projectID: projectID, - UserID: userID, - GrantID: grantID, - Roles: roles, + UserID: userID, + GrantID: grantID, + Roles: roles, } } @@ -134,9 +131,8 @@ func GrantMemberChangedEventMapper(event *repository.Event) (eventstore.EventRea type GrantMemberRemovedEvent struct { eventstore.BaseEvent `json:"-"` - UserID string `json:"userId"` - GrantID string `json:"grantId"` - projectID string + UserID string `json:"userId"` + GrantID string `json:"grantId"` } func (e *GrantMemberRemovedEvent) Data() interface{} { @@ -144,13 +140,12 @@ func (e *GrantMemberRemovedEvent) Data() interface{} { } func (e *GrantMemberRemovedEvent) UniqueConstraints() []*eventstore.EventUniqueConstraint { - return []*eventstore.EventUniqueConstraint{NewRemoveProjectGrantMemberUniqueConstraint(e.projectID, e.UserID, e.GrantID)} + return []*eventstore.EventUniqueConstraint{NewRemoveProjectGrantMemberUniqueConstraint(e.Aggregate().ID, e.UserID, e.GrantID)} } func NewProjectGrantMemberRemovedEvent( ctx context.Context, aggregate *eventstore.Aggregate, - projectID, userID, grantID string, ) *GrantMemberRemovedEvent { @@ -160,9 +155,8 @@ func NewProjectGrantMemberRemovedEvent( aggregate, GrantMemberRemovedType, ), - UserID: userID, - GrantID: grantID, - projectID: projectID, + UserID: userID, + GrantID: grantID, } } diff --git a/internal/repository/project/role.go b/internal/repository/project/role.go index e51b3ed3d..0efd08c3c 100644 --- a/internal/repository/project/role.go +++ b/internal/repository/project/role.go @@ -37,7 +37,6 @@ type RoleAddedEvent struct { Key string `json:"key,omitempty"` DisplayName string `json:"displayName,omitempty"` Group string `json:"group,omitempty"` - projectID string } func (e *RoleAddedEvent) Data() interface{} { @@ -45,7 +44,7 @@ func (e *RoleAddedEvent) Data() interface{} { } func (e *RoleAddedEvent) UniqueConstraints() []*eventstore.EventUniqueConstraint { - return []*eventstore.EventUniqueConstraint{NewAddProjectRoleUniqueConstraint(e.Key, e.projectID)} + return []*eventstore.EventUniqueConstraint{NewAddProjectRoleUniqueConstraint(e.Key, e.Aggregate().ID)} } func NewRoleAddedEvent( @@ -53,8 +52,7 @@ func NewRoleAddedEvent( aggregate *eventstore.Aggregate, key, displayName, - group, - projectID string, + group string, ) *RoleAddedEvent { return &RoleAddedEvent{ BaseEvent: *eventstore.NewBaseEventForPush( @@ -65,7 +63,6 @@ func NewRoleAddedEvent( Key: key, DisplayName: displayName, Group: group, - projectID: projectID, } } @@ -101,6 +98,7 @@ func (e *RoleChangedEvent) UniqueConstraints() []*eventstore.EventUniqueConstrai func NewRoleChangedEvent( ctx context.Context, aggregate *eventstore.Aggregate, + key string, changes []RoleChanges, ) (*RoleChangedEvent, error) { if len(changes) == 0 { @@ -112,6 +110,7 @@ func NewRoleChangedEvent( aggregate, RoleChangedType, ), + Key: key, } for _, change := range changes { change(changeEvent) @@ -154,8 +153,7 @@ func RoleChangedEventMapper(event *repository.Event) (eventstore.EventReader, er type RoleRemovedEvent struct { eventstore.BaseEvent `json:"-"` - Key string `json:"key,omitempty"` - projectID string `json:"-"` + Key string `json:"key,omitempty"` } func (e *RoleRemovedEvent) Data() interface{} { @@ -163,22 +161,20 @@ func (e *RoleRemovedEvent) Data() interface{} { } func (e *RoleRemovedEvent) UniqueConstraints() []*eventstore.EventUniqueConstraint { - return []*eventstore.EventUniqueConstraint{NewRemoveProjectRoleUniqueConstraint(e.Key, e.projectID)} + return []*eventstore.EventUniqueConstraint{NewRemoveProjectRoleUniqueConstraint(e.Key, e.Aggregate().ID)} } func NewRoleRemovedEvent( ctx context.Context, aggregate *eventstore.Aggregate, - key, - projectID string) *RoleRemovedEvent { + key string) *RoleRemovedEvent { return &RoleRemovedEvent{ BaseEvent: *eventstore.NewBaseEventForPush( ctx, aggregate, RoleRemovedType, ), - Key: key, - projectID: projectID, + Key: key, } } diff --git a/internal/repository/user/aggregate.go b/internal/repository/user/aggregate.go index 8391cf68c..db857bcbf 100644 --- a/internal/repository/user/aggregate.go +++ b/internal/repository/user/aggregate.go @@ -12,3 +12,14 @@ const ( type Aggregate struct { eventstore.Aggregate } + +func NewAggregate(id, resourceOwner string) *Aggregate { + return &Aggregate{ + Aggregate: eventstore.Aggregate{ + Typ: AggregateType, + Version: AggregateVersion, + ID: id, + ResourceOwner: resourceOwner, + }, + } +} diff --git a/internal/repository/usergrant/aggregate.go b/internal/repository/usergrant/aggregate.go index 68fa1f0c7..00c9efe9c 100644 --- a/internal/repository/usergrant/aggregate.go +++ b/internal/repository/usergrant/aggregate.go @@ -12,3 +12,14 @@ const ( type Aggregate struct { eventstore.Aggregate } + +func NewAggregate(id, resourceOwner string) *Aggregate { + return &Aggregate{ + Aggregate: eventstore.Aggregate{ + Typ: AggregateType, + Version: AggregateVersion, + ID: id, + ResourceOwner: resourceOwner, + }, + } +} diff --git a/proto/zitadel/management.proto b/proto/zitadel/management.proto index 77e48b105..99f797631 100644 --- a/proto/zitadel/management.proto +++ b/proto/zitadel/management.proto @@ -2860,14 +2860,7 @@ message BulkRemoveUserGrantRequest { repeated string grant_id = 1; } -message BulkRemoveUserGrantResponse { - message RemovedUserGrant { - zitadel.v1.ObjectDetails details = 1; - string grant_id = 2; - } - - repeated RemovedUserGrant result = 1; -} +message BulkRemoveUserGrantResponse {} message GetOrgIAMPolicyRequest {}