mirror of
https://github.com/zitadel/zitadel.git
synced 2025-01-10 15:53:39 +00:00
feat: SetPassword endpoint
This commit is contained in:
parent
aa13e451e3
commit
1e9d58c924
@ -0,0 +1,234 @@
|
|||||||
|
//go:build integration
|
||||||
|
|
||||||
|
package user_test
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/brianvoe/gofakeit/v6"
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
|
"github.com/stretchr/testify/require"
|
||||||
|
"google.golang.org/protobuf/types/known/timestamppb"
|
||||||
|
|
||||||
|
"github.com/zitadel/zitadel/internal/integration"
|
||||||
|
object "github.com/zitadel/zitadel/pkg/grpc/object/v3alpha"
|
||||||
|
resource_object "github.com/zitadel/zitadel/pkg/grpc/resources/object/v3alpha"
|
||||||
|
user "github.com/zitadel/zitadel/pkg/grpc/resources/user/v3alpha"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestServer_SetPassword(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
instance := integration.NewInstance(CTX)
|
||||||
|
ensureFeatureEnabled(t, instance)
|
||||||
|
isolatedIAMOwnerCTX := instance.WithAuthorization(CTX, integration.UserTypeIAMOwner)
|
||||||
|
|
||||||
|
schema := []byte(`{
|
||||||
|
"$schema": "urn:zitadel:schema:v1",
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"name": {
|
||||||
|
"type": "string"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}`)
|
||||||
|
schemaResp := instance.CreateUserSchema(isolatedIAMOwnerCTX, schema)
|
||||||
|
orgResp := instance.CreateOrganization(isolatedIAMOwnerCTX, gofakeit.Name(), gofakeit.Email())
|
||||||
|
|
||||||
|
type res struct {
|
||||||
|
want *resource_object.Details
|
||||||
|
}
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
ctx context.Context
|
||||||
|
dep func(req *user.SetPasswordRequest) error
|
||||||
|
req *user.SetPasswordRequest
|
||||||
|
res res
|
||||||
|
wantErr bool
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
name: "password set, no context",
|
||||||
|
ctx: context.Background(),
|
||||||
|
dep: func(req *user.SetPasswordRequest) error {
|
||||||
|
userResp := instance.CreateSchemaUser(isolatedIAMOwnerCTX, orgResp.GetOrganizationId(), schemaResp.GetDetails().GetId(), []byte("{\"name\": \"user\"}"))
|
||||||
|
req.Id = userResp.GetDetails().GetId()
|
||||||
|
return nil
|
||||||
|
},
|
||||||
|
req: &user.SetPasswordRequest{
|
||||||
|
Organization: &object.Organization{
|
||||||
|
Property: &object.Organization_OrgId{
|
||||||
|
OrgId: orgResp.GetOrganizationId(),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
NewPassword: &user.SetPassword{
|
||||||
|
Type: &user.SetPassword_Password{
|
||||||
|
Password: gofakeit.Password(true, true, true, true, false, 12),
|
||||||
|
},
|
||||||
|
ChangeRequired: false,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
wantErr: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "password set, no permission",
|
||||||
|
ctx: instance.WithAuthorization(CTX, integration.UserTypeLogin),
|
||||||
|
dep: func(req *user.SetPasswordRequest) error {
|
||||||
|
userResp := instance.CreateSchemaUser(isolatedIAMOwnerCTX, orgResp.GetOrganizationId(), schemaResp.GetDetails().GetId(), []byte("{\"name\": \"user\"}"))
|
||||||
|
req.Id = userResp.GetDetails().GetId()
|
||||||
|
return nil
|
||||||
|
},
|
||||||
|
req: &user.SetPasswordRequest{
|
||||||
|
Organization: &object.Organization{
|
||||||
|
Property: &object.Organization_OrgId{
|
||||||
|
OrgId: orgResp.GetOrganizationId(),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
NewPassword: &user.SetPassword{
|
||||||
|
Type: &user.SetPassword_Password{
|
||||||
|
Password: gofakeit.Password(true, true, true, true, false, 12),
|
||||||
|
},
|
||||||
|
ChangeRequired: false,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
wantErr: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "password set, password empty",
|
||||||
|
ctx: instance.WithAuthorization(CTX, integration.UserTypeLogin),
|
||||||
|
dep: func(req *user.SetPasswordRequest) error {
|
||||||
|
userResp := instance.CreateSchemaUser(isolatedIAMOwnerCTX, orgResp.GetOrganizationId(), schemaResp.GetDetails().GetId(), []byte("{\"name\": \"user\"}"))
|
||||||
|
req.Id = userResp.GetDetails().GetId()
|
||||||
|
return nil
|
||||||
|
},
|
||||||
|
req: &user.SetPasswordRequest{
|
||||||
|
Organization: &object.Organization{
|
||||||
|
Property: &object.Organization_OrgId{
|
||||||
|
OrgId: orgResp.GetOrganizationId(),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
NewPassword: &user.SetPassword{
|
||||||
|
Type: &user.SetPassword_Password{
|
||||||
|
Password: "",
|
||||||
|
},
|
||||||
|
ChangeRequired: false,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
wantErr: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "password set, user not existing in org",
|
||||||
|
ctx: isolatedIAMOwnerCTX,
|
||||||
|
dep: func(req *user.SetPasswordRequest) error {
|
||||||
|
userResp := instance.CreateSchemaUser(isolatedIAMOwnerCTX, orgResp.GetOrganizationId(), schemaResp.GetDetails().GetId(), []byte("{\"name\": \"user\"}"))
|
||||||
|
req.Id = userResp.GetDetails().GetId()
|
||||||
|
return nil
|
||||||
|
},
|
||||||
|
req: &user.SetPasswordRequest{
|
||||||
|
Organization: &object.Organization{
|
||||||
|
Property: &object.Organization_OrgId{
|
||||||
|
OrgId: "notexisting",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
NewPassword: &user.SetPassword{
|
||||||
|
Type: &user.SetPassword_Password{
|
||||||
|
Password: gofakeit.Password(true, true, true, true, false, 12),
|
||||||
|
},
|
||||||
|
ChangeRequired: false,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
wantErr: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "username add, user not existing",
|
||||||
|
ctx: isolatedIAMOwnerCTX,
|
||||||
|
|
||||||
|
req: &user.SetPasswordRequest{
|
||||||
|
Organization: &object.Organization{
|
||||||
|
Property: &object.Organization_OrgId{
|
||||||
|
OrgId: "notexisting",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
Id: "not existing",
|
||||||
|
NewPassword: &user.SetPassword{
|
||||||
|
Type: &user.SetPassword_Password{
|
||||||
|
Password: gofakeit.Password(true, true, true, true, false, 12),
|
||||||
|
},
|
||||||
|
ChangeRequired: false,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
wantErr: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "password set, ok",
|
||||||
|
ctx: isolatedIAMOwnerCTX,
|
||||||
|
dep: func(req *user.SetPasswordRequest) error {
|
||||||
|
userResp := instance.CreateSchemaUser(isolatedIAMOwnerCTX, orgResp.GetOrganizationId(), schemaResp.GetDetails().GetId(), []byte("{\"name\": \"user\"}"))
|
||||||
|
req.Id = userResp.GetDetails().GetId()
|
||||||
|
return nil
|
||||||
|
},
|
||||||
|
req: &user.SetPasswordRequest{
|
||||||
|
Organization: &object.Organization{
|
||||||
|
Property: &object.Organization_OrgId{
|
||||||
|
OrgId: orgResp.GetOrganizationId(),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
NewPassword: &user.SetPassword{
|
||||||
|
Type: &user.SetPassword_Password{
|
||||||
|
Password: gofakeit.Password(true, true, true, true, false, 12),
|
||||||
|
},
|
||||||
|
ChangeRequired: false,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
res: res{
|
||||||
|
want: &resource_object.Details{
|
||||||
|
Changed: timestamppb.Now(),
|
||||||
|
Owner: &object.Owner{
|
||||||
|
Type: object.OwnerType_OWNER_TYPE_ORG,
|
||||||
|
Id: orgResp.GetOrganizationId(),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "password set, no org, ok",
|
||||||
|
ctx: isolatedIAMOwnerCTX,
|
||||||
|
dep: func(req *user.SetPasswordRequest) error {
|
||||||
|
userResp := instance.CreateSchemaUser(isolatedIAMOwnerCTX, orgResp.GetOrganizationId(), schemaResp.GetDetails().GetId(), []byte("{\"name\": \"user\"}"))
|
||||||
|
req.Id = userResp.GetDetails().GetId()
|
||||||
|
return nil
|
||||||
|
},
|
||||||
|
req: &user.SetPasswordRequest{
|
||||||
|
NewPassword: &user.SetPassword{
|
||||||
|
Type: &user.SetPassword_Password{
|
||||||
|
Password: gofakeit.Password(true, true, true, true, false, 12),
|
||||||
|
},
|
||||||
|
ChangeRequired: false,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
res: res{
|
||||||
|
want: &resource_object.Details{
|
||||||
|
Changed: timestamppb.Now(),
|
||||||
|
Owner: &object.Owner{
|
||||||
|
Type: object.OwnerType_OWNER_TYPE_ORG,
|
||||||
|
Id: orgResp.GetOrganizationId(),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
for _, tt := range tests {
|
||||||
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
|
if tt.dep != nil {
|
||||||
|
err := tt.dep(tt.req)
|
||||||
|
assert.NoError(t, err)
|
||||||
|
}
|
||||||
|
got, err := instance.Client.UserV3Alpha.SetPassword(tt.ctx, tt.req)
|
||||||
|
if tt.wantErr {
|
||||||
|
require.Error(t, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
require.NoError(t, err)
|
||||||
|
integration.AssertResourceDetails(t, tt.res.want, got.Details)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
33
internal/api/grpc/resources/user/v3alpha/password.go
Normal file
33
internal/api/grpc/resources/user/v3alpha/password.go
Normal file
@ -0,0 +1,33 @@
|
|||||||
|
package user
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
|
||||||
|
resource_object "github.com/zitadel/zitadel/internal/api/grpc/resources/object/v3alpha"
|
||||||
|
"github.com/zitadel/zitadel/internal/command"
|
||||||
|
object "github.com/zitadel/zitadel/pkg/grpc/object/v3alpha"
|
||||||
|
user "github.com/zitadel/zitadel/pkg/grpc/resources/user/v3alpha"
|
||||||
|
)
|
||||||
|
|
||||||
|
func (s *Server) SetPassword(ctx context.Context, req *user.SetPasswordRequest) (_ *user.SetPasswordResponse, err error) {
|
||||||
|
if err := checkUserSchemaEnabled(ctx); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
details, err := s.command.SetSchemaUserPassword(ctx, setPasswordRequestToSetSchemaUserPassword(req))
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return &user.SetPasswordResponse{
|
||||||
|
Details: resource_object.DomainToDetailsPb(details, object.OwnerType_OWNER_TYPE_ORG, details.ResourceOwner),
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func setPasswordRequestToSetSchemaUserPassword(req *user.SetPasswordRequest) *command.SetSchemaUserPassword {
|
||||||
|
return &command.SetSchemaUserPassword{
|
||||||
|
ResourceOwner: organizationToUpdateResourceOwner(req.Organization),
|
||||||
|
UserID: req.GetId(),
|
||||||
|
Password: req.GetNewPassword().GetPassword(),
|
||||||
|
EncodedPasswordHash: req.GetNewPassword().GetHash(),
|
||||||
|
ChangeRequired: req.GetNewPassword().GetChangeRequired(),
|
||||||
|
}
|
||||||
|
}
|
123
internal/command/user_v3_password.go
Normal file
123
internal/command/user_v3_password.go
Normal file
@ -0,0 +1,123 @@
|
|||||||
|
package command
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
|
||||||
|
"github.com/zitadel/zitadel/internal/crypto"
|
||||||
|
"github.com/zitadel/zitadel/internal/domain"
|
||||||
|
"github.com/zitadel/zitadel/internal/repository/user/authenticator"
|
||||||
|
"github.com/zitadel/zitadel/internal/zerrors"
|
||||||
|
)
|
||||||
|
|
||||||
|
type SetSchemaUserPassword struct {
|
||||||
|
ResourceOwner string
|
||||||
|
UserID string
|
||||||
|
|
||||||
|
Password string
|
||||||
|
EncodedPasswordHash string
|
||||||
|
ChangeRequired bool
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *SetSchemaUserPassword) Validate(hasher *crypto.Hasher) (err error) {
|
||||||
|
if p.UserID == "" {
|
||||||
|
return zerrors.ThrowInvalidArgument(nil, "COMMAND-aS3Vz5t6BS", "Errors.IDMissing")
|
||||||
|
}
|
||||||
|
|
||||||
|
if p.EncodedPasswordHash != "" {
|
||||||
|
if !hasher.EncodingSupported(p.EncodedPasswordHash) {
|
||||||
|
return zerrors.ThrowInvalidArgument(nil, "COMMAND-oz74onzvqr", "Errors.User.Password.NotSupported")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if p.Password == "" && p.EncodedPasswordHash == "" {
|
||||||
|
return zerrors.ThrowInvalidArgument(nil, "COMMAND-3klek4sbns", "Errors.User.Password.Empty")
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Commands) SetSchemaUserPassword(ctx context.Context, username *SetSchemaUserPassword) (*domain.ObjectDetails, error) {
|
||||||
|
if err := username.Validate(c.userPasswordHasher); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
existing, err := c.getPasswordExistsWithVerification(ctx, username.ResourceOwner, username.UserID)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
resourceOwner := existing.ResourceOwner
|
||||||
|
if existing.EncodedHash == "" {
|
||||||
|
existingUser, err := c.getSchemaUserExists(ctx, username.ResourceOwner, username.UserID)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
if !existingUser.Exists() {
|
||||||
|
return nil, zerrors.ThrowNotFound(nil, "TODO", "TODO")
|
||||||
|
}
|
||||||
|
resourceOwner = existingUser.ResourceOwner
|
||||||
|
}
|
||||||
|
|
||||||
|
// If password is provided, let's check if is compliant with the policy.
|
||||||
|
// If only a encodedPassword is passed, we can skip this.
|
||||||
|
if username.Password != "" {
|
||||||
|
if err = c.checkPasswordComplexity(ctx, username.Password, resourceOwner); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
encodedPassword := username.EncodedPasswordHash
|
||||||
|
if username.Password != "" {
|
||||||
|
encodedPassword, err = c.userPasswordHasher.Hash(username.Password)
|
||||||
|
if err = convertPasswapErr(err); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
events, err := c.eventstore.Push(ctx,
|
||||||
|
authenticator.NewPasswordCreatedEvent(ctx,
|
||||||
|
&authenticator.NewAggregate(username.UserID, resourceOwner).Aggregate,
|
||||||
|
existing.UserID,
|
||||||
|
encodedPassword,
|
||||||
|
username.ChangeRequired,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return pushedEventsToObjectDetails(events), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Commands) DeleteSchemaUserPassword(ctx context.Context, resourceOwner, id string) (_ *domain.ObjectDetails, err error) {
|
||||||
|
existing, err := c.getPasswordExistsWithVerification(ctx, resourceOwner, id)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
if existing.EncodedHash == "" {
|
||||||
|
return nil, zerrors.ThrowNotFound(nil, "TODO", "TODO")
|
||||||
|
}
|
||||||
|
|
||||||
|
events, err := c.eventstore.Push(ctx,
|
||||||
|
authenticator.NewPasswordDeletedEvent(ctx,
|
||||||
|
&authenticator.NewAggregate(id, existing.ResourceOwner).Aggregate,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return pushedEventsToObjectDetails(events), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Commands) getPasswordExistsWithVerification(ctx context.Context, resourceOwner, id string) (*PasswordV3WriteModel, error) {
|
||||||
|
if id == "" {
|
||||||
|
return nil, zerrors.ThrowInvalidArgument(nil, "COMMAND-PoSU5BOZCi", "Errors.IDMissing")
|
||||||
|
}
|
||||||
|
writeModel := NewPasswordV3WriteModel(resourceOwner, id)
|
||||||
|
if err := c.eventstore.FilterToQueryReducer(ctx, writeModel); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO permission through old password and password code
|
||||||
|
if err := c.checkPermissionUpdateUser(ctx, writeModel.ResourceOwner, writeModel.UserID); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return writeModel, nil
|
||||||
|
}
|
53
internal/command/user_v3_password_model.go
Normal file
53
internal/command/user_v3_password_model.go
Normal file
@ -0,0 +1,53 @@
|
|||||||
|
package command
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/zitadel/zitadel/internal/eventstore"
|
||||||
|
"github.com/zitadel/zitadel/internal/repository/user/authenticator"
|
||||||
|
"github.com/zitadel/zitadel/internal/repository/user/schemauser"
|
||||||
|
)
|
||||||
|
|
||||||
|
type PasswordV3WriteModel struct {
|
||||||
|
eventstore.WriteModel
|
||||||
|
UserID string
|
||||||
|
|
||||||
|
EncodedHash string
|
||||||
|
ChangeRequired bool
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewPasswordV3WriteModel(resourceOwner, id string) *PasswordV3WriteModel {
|
||||||
|
return &PasswordV3WriteModel{
|
||||||
|
WriteModel: eventstore.WriteModel{
|
||||||
|
AggregateID: id,
|
||||||
|
ResourceOwner: resourceOwner,
|
||||||
|
},
|
||||||
|
UserID: id,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (wm *PasswordV3WriteModel) Reduce() error {
|
||||||
|
for _, event := range wm.Events {
|
||||||
|
switch e := event.(type) {
|
||||||
|
case *authenticator.PasswordCreatedEvent:
|
||||||
|
wm.UserID = e.UserID
|
||||||
|
wm.EncodedHash = e.EncodedHash
|
||||||
|
wm.ChangeRequired = e.ChangeRequired
|
||||||
|
case *authenticator.PasswordDeletedEvent:
|
||||||
|
wm.UserID = ""
|
||||||
|
wm.EncodedHash = ""
|
||||||
|
wm.ChangeRequired = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return wm.WriteModel.Reduce()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (wm *PasswordV3WriteModel) Query() *eventstore.SearchQueryBuilder {
|
||||||
|
return eventstore.NewSearchQueryBuilder(eventstore.ColumnsEvent).
|
||||||
|
ResourceOwner(wm.ResourceOwner).
|
||||||
|
AddQuery().
|
||||||
|
AggregateTypes(schemauser.AggregateType).
|
||||||
|
AggregateIDs(wm.AggregateID).
|
||||||
|
EventTypes(
|
||||||
|
authenticator.PasswordCreatedType,
|
||||||
|
authenticator.PasswordDeletedType,
|
||||||
|
).Builder()
|
||||||
|
}
|
416
internal/command/user_v3_password_test.go
Normal file
416
internal/command/user_v3_password_test.go
Normal file
@ -0,0 +1,416 @@
|
|||||||
|
package command
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"errors"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
|
|
||||||
|
"github.com/zitadel/zitadel/internal/api/authz"
|
||||||
|
"github.com/zitadel/zitadel/internal/crypto"
|
||||||
|
"github.com/zitadel/zitadel/internal/domain"
|
||||||
|
"github.com/zitadel/zitadel/internal/eventstore"
|
||||||
|
"github.com/zitadel/zitadel/internal/repository/org"
|
||||||
|
"github.com/zitadel/zitadel/internal/repository/user/authenticator"
|
||||||
|
"github.com/zitadel/zitadel/internal/zerrors"
|
||||||
|
)
|
||||||
|
|
||||||
|
func filterSchemaUserPasswordExisting() expect {
|
||||||
|
return expectFilter(
|
||||||
|
eventFromEventPusher(
|
||||||
|
authenticator.NewPasswordCreatedEvent(
|
||||||
|
context.Background(),
|
||||||
|
&authenticator.NewAggregate("user1", "org1").Aggregate,
|
||||||
|
"user1",
|
||||||
|
"encoded",
|
||||||
|
false,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
func filterPasswordComplexityPolicyExisting() expect {
|
||||||
|
return expectFilter(
|
||||||
|
eventFromEventPusher(
|
||||||
|
org.NewPasswordComplexityPolicyAddedEvent(context.Background(),
|
||||||
|
&org.NewAggregate("org1").Aggregate,
|
||||||
|
1,
|
||||||
|
false,
|
||||||
|
false,
|
||||||
|
false,
|
||||||
|
false,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestCommands_SetSchemaUserPassword(t *testing.T) {
|
||||||
|
type fields struct {
|
||||||
|
eventstore func(t *testing.T) *eventstore.Eventstore
|
||||||
|
userPasswordHasher *crypto.Hasher
|
||||||
|
checkPermission domain.PermissionCheck
|
||||||
|
}
|
||||||
|
type args struct {
|
||||||
|
ctx context.Context
|
||||||
|
user *SetSchemaUserPassword
|
||||||
|
}
|
||||||
|
type res struct {
|
||||||
|
details *domain.ObjectDetails
|
||||||
|
err func(error) bool
|
||||||
|
}
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
fields fields
|
||||||
|
args args
|
||||||
|
res res
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
"no userID, error",
|
||||||
|
fields{
|
||||||
|
eventstore: expectEventstore(),
|
||||||
|
checkPermission: newMockPermissionCheckAllowed(),
|
||||||
|
},
|
||||||
|
args{
|
||||||
|
ctx: authz.NewMockContext("instanceID", "", ""),
|
||||||
|
user: &SetSchemaUserPassword{},
|
||||||
|
},
|
||||||
|
res{
|
||||||
|
err: func(err error) bool {
|
||||||
|
return errors.Is(err, zerrors.ThrowInvalidArgument(nil, "COMMAND-aS3Vz5t6BS", "Errors.IDMissing"))
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"no password, error",
|
||||||
|
fields{
|
||||||
|
eventstore: expectEventstore(),
|
||||||
|
checkPermission: newMockPermissionCheckAllowed(),
|
||||||
|
},
|
||||||
|
args{
|
||||||
|
ctx: authz.NewMockContext("instanceID", "", ""),
|
||||||
|
user: &SetSchemaUserPassword{
|
||||||
|
UserID: "user1",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
res{
|
||||||
|
err: func(err error) bool {
|
||||||
|
return errors.Is(err, zerrors.ThrowInvalidArgument(nil, "COMMAND-3klek4sbns", "Errors.User.Password.Empty"))
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"user not existing, error",
|
||||||
|
fields{
|
||||||
|
eventstore: expectEventstore(
|
||||||
|
expectFilter(),
|
||||||
|
expectFilter(),
|
||||||
|
),
|
||||||
|
checkPermission: newMockPermissionCheckAllowed(),
|
||||||
|
},
|
||||||
|
args{
|
||||||
|
ctx: authz.NewMockContext("instanceID", "", ""),
|
||||||
|
user: &SetSchemaUserPassword{
|
||||||
|
UserID: "notexisting",
|
||||||
|
Password: "password",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
res{
|
||||||
|
err: func(err error) bool {
|
||||||
|
return errors.Is(err, zerrors.ThrowNotFound(nil, "TODO", "TODO"))
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"no permission, error",
|
||||||
|
fields{
|
||||||
|
eventstore: expectEventstore(
|
||||||
|
expectFilter(),
|
||||||
|
),
|
||||||
|
checkPermission: newMockPermissionCheckNotAllowed(),
|
||||||
|
},
|
||||||
|
args{
|
||||||
|
ctx: authz.NewMockContext("instanceID", "", ""),
|
||||||
|
user: &SetSchemaUserPassword{
|
||||||
|
UserID: "user1",
|
||||||
|
Password: "password",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
res{
|
||||||
|
err: func(err error) bool {
|
||||||
|
return errors.Is(err, zerrors.ThrowPermissionDenied(nil, "AUTHZ-HKJD33", "Errors.PermissionDenied"))
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"password added, ok",
|
||||||
|
fields{
|
||||||
|
eventstore: expectEventstore(
|
||||||
|
expectFilter(),
|
||||||
|
filterSchemaUserExisting(),
|
||||||
|
filterPasswordComplexityPolicyExisting(),
|
||||||
|
expectPush(
|
||||||
|
authenticator.NewPasswordCreatedEvent(
|
||||||
|
context.Background(),
|
||||||
|
&authenticator.NewAggregate("user1", "org1").Aggregate,
|
||||||
|
"user1",
|
||||||
|
"$plain$x$password",
|
||||||
|
false,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
checkPermission: newMockPermissionCheckAllowed(),
|
||||||
|
userPasswordHasher: mockPasswordHasher("x"),
|
||||||
|
},
|
||||||
|
args{
|
||||||
|
ctx: authz.NewMockContext("instanceID", "", ""),
|
||||||
|
user: &SetSchemaUserPassword{
|
||||||
|
UserID: "user1",
|
||||||
|
Password: "password",
|
||||||
|
ChangeRequired: false,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
res{
|
||||||
|
details: &domain.ObjectDetails{
|
||||||
|
ResourceOwner: "org1",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"password set, ok",
|
||||||
|
fields{
|
||||||
|
eventstore: expectEventstore(
|
||||||
|
filterSchemaUserPasswordExisting(),
|
||||||
|
filterPasswordComplexityPolicyExisting(),
|
||||||
|
expectPush(
|
||||||
|
authenticator.NewPasswordCreatedEvent(
|
||||||
|
context.Background(),
|
||||||
|
&authenticator.NewAggregate("user1", "org1").Aggregate,
|
||||||
|
"user1",
|
||||||
|
"$plain$x$password",
|
||||||
|
false,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
checkPermission: newMockPermissionCheckAllowed(),
|
||||||
|
userPasswordHasher: mockPasswordHasher("x"),
|
||||||
|
},
|
||||||
|
args{
|
||||||
|
ctx: authz.NewMockContext("instanceID", "", ""),
|
||||||
|
user: &SetSchemaUserPassword{
|
||||||
|
UserID: "user1",
|
||||||
|
Password: "password",
|
||||||
|
ChangeRequired: false,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
res{
|
||||||
|
details: &domain.ObjectDetails{
|
||||||
|
ResourceOwner: "org1",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"password set, changeRequired, ok",
|
||||||
|
fields{
|
||||||
|
eventstore: expectEventstore(
|
||||||
|
filterSchemaUserPasswordExisting(),
|
||||||
|
filterPasswordComplexityPolicyExisting(),
|
||||||
|
expectPush(
|
||||||
|
authenticator.NewPasswordCreatedEvent(
|
||||||
|
context.Background(),
|
||||||
|
&authenticator.NewAggregate("user1", "org1").Aggregate,
|
||||||
|
"user1",
|
||||||
|
"$plain$x$password",
|
||||||
|
true,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
checkPermission: newMockPermissionCheckAllowed(),
|
||||||
|
userPasswordHasher: mockPasswordHasher("x"),
|
||||||
|
},
|
||||||
|
args{
|
||||||
|
ctx: authz.NewMockContext("instanceID", "", ""),
|
||||||
|
user: &SetSchemaUserPassword{
|
||||||
|
UserID: "user1",
|
||||||
|
Password: "password",
|
||||||
|
ChangeRequired: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
res{
|
||||||
|
details: &domain.ObjectDetails{
|
||||||
|
ResourceOwner: "org1",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
for _, tt := range tests {
|
||||||
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
|
c := &Commands{
|
||||||
|
eventstore: tt.fields.eventstore(t),
|
||||||
|
checkPermission: tt.fields.checkPermission,
|
||||||
|
userPasswordHasher: tt.fields.userPasswordHasher,
|
||||||
|
}
|
||||||
|
details, err := c.SetSchemaUserPassword(tt.args.ctx, tt.args.user)
|
||||||
|
if tt.res.err == nil {
|
||||||
|
assert.NoError(t, err)
|
||||||
|
}
|
||||||
|
if tt.res.err != nil && !tt.res.err(err) {
|
||||||
|
t.Errorf("got wrong err: %v ", err)
|
||||||
|
}
|
||||||
|
if tt.res.err == nil {
|
||||||
|
assertObjectDetails(t, tt.res.details, details)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestCommands_DeleteSchemaUserPassword(t *testing.T) {
|
||||||
|
type fields struct {
|
||||||
|
eventstore func(t *testing.T) *eventstore.Eventstore
|
||||||
|
checkPermission domain.PermissionCheck
|
||||||
|
}
|
||||||
|
type args struct {
|
||||||
|
ctx context.Context
|
||||||
|
resourceOwner string
|
||||||
|
id string
|
||||||
|
}
|
||||||
|
type res struct {
|
||||||
|
details *domain.ObjectDetails
|
||||||
|
err func(error) bool
|
||||||
|
}
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
fields fields
|
||||||
|
args args
|
||||||
|
res res
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
"no ID, error",
|
||||||
|
fields{
|
||||||
|
eventstore: expectEventstore(),
|
||||||
|
checkPermission: newMockPermissionCheckAllowed(),
|
||||||
|
},
|
||||||
|
args{
|
||||||
|
ctx: authz.NewMockContext("instanceID", "", ""),
|
||||||
|
id: "",
|
||||||
|
},
|
||||||
|
res{
|
||||||
|
err: func(err error) bool {
|
||||||
|
return errors.Is(err, zerrors.ThrowInvalidArgument(nil, "COMMAND-PoSU5BOZCi", "Errors.IDMissing"))
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"password not existing, error",
|
||||||
|
fields{
|
||||||
|
eventstore: expectEventstore(
|
||||||
|
expectFilter(),
|
||||||
|
),
|
||||||
|
checkPermission: newMockPermissionCheckAllowed(),
|
||||||
|
},
|
||||||
|
args{
|
||||||
|
ctx: authz.NewMockContext("instanceID", "", ""),
|
||||||
|
id: "notexisting",
|
||||||
|
},
|
||||||
|
res{
|
||||||
|
err: func(err error) bool {
|
||||||
|
return errors.Is(err, zerrors.ThrowNotFound(nil, "TODO", "TODO"))
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"password already removed, error",
|
||||||
|
fields{
|
||||||
|
eventstore: expectEventstore(
|
||||||
|
expectFilter(
|
||||||
|
eventFromEventPusher(
|
||||||
|
authenticator.NewPasswordCreatedEvent(
|
||||||
|
context.Background(),
|
||||||
|
&authenticator.NewAggregate("user1", "org1").Aggregate,
|
||||||
|
"id1",
|
||||||
|
"hash",
|
||||||
|
false,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
eventFromEventPusher(
|
||||||
|
authenticator.NewPasswordDeletedEvent(
|
||||||
|
context.Background(),
|
||||||
|
&authenticator.NewAggregate("user1", "org1").Aggregate,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
checkPermission: newMockPermissionCheckAllowed(),
|
||||||
|
},
|
||||||
|
args{
|
||||||
|
ctx: authz.NewMockContext("instanceID", "", ""),
|
||||||
|
id: "user1",
|
||||||
|
},
|
||||||
|
res{
|
||||||
|
err: func(err error) bool {
|
||||||
|
return errors.Is(err, zerrors.ThrowNotFound(nil, "TODO", "TODO"))
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"no permission, error",
|
||||||
|
fields{
|
||||||
|
eventstore: expectEventstore(
|
||||||
|
filterSchemaUserPasswordExisting(),
|
||||||
|
),
|
||||||
|
checkPermission: newMockPermissionCheckNotAllowed(),
|
||||||
|
},
|
||||||
|
args{
|
||||||
|
ctx: authz.NewMockContext("instanceID", "", ""),
|
||||||
|
id: "user1",
|
||||||
|
},
|
||||||
|
res{
|
||||||
|
err: func(err error) bool {
|
||||||
|
return errors.Is(err, zerrors.ThrowPermissionDenied(nil, "AUTHZ-HKJD33", "Errors.PermissionDenied"))
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"password removed, ok",
|
||||||
|
fields{
|
||||||
|
eventstore: expectEventstore(
|
||||||
|
filterSchemaUserPasswordExisting(),
|
||||||
|
expectPush(
|
||||||
|
authenticator.NewPasswordDeletedEvent(
|
||||||
|
context.Background(),
|
||||||
|
&authenticator.NewAggregate("user1", "org1").Aggregate,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
checkPermission: newMockPermissionCheckAllowed(),
|
||||||
|
},
|
||||||
|
args{
|
||||||
|
ctx: authz.NewMockContext("instanceID", "", ""),
|
||||||
|
id: "user1",
|
||||||
|
},
|
||||||
|
res{
|
||||||
|
details: &domain.ObjectDetails{
|
||||||
|
ResourceOwner: "org1",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
for _, tt := range tests {
|
||||||
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
|
c := &Commands{
|
||||||
|
eventstore: tt.fields.eventstore(t),
|
||||||
|
checkPermission: tt.fields.checkPermission,
|
||||||
|
}
|
||||||
|
details, err := c.DeleteSchemaUserPassword(tt.args.ctx, tt.args.resourceOwner, tt.args.id)
|
||||||
|
if tt.res.err == nil {
|
||||||
|
assert.NoError(t, err)
|
||||||
|
}
|
||||||
|
if tt.res.err != nil && !tt.res.err(err) {
|
||||||
|
t.Errorf("got wrong err: %v ", err)
|
||||||
|
}
|
||||||
|
if tt.res.err == nil {
|
||||||
|
assertObjectDetails(t, tt.res.details, details)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
@ -75,6 +75,34 @@ func (c *Commands) getSchemaUsernameExistsWithPermission(ctx context.Context, re
|
|||||||
return writeModel, nil
|
return writeModel, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func existingSchemaUser(ctx context.Context, c *Commands, resourceOwner, userID string) (*UserV3WriteModel, error) {
|
||||||
|
if userID == "" {
|
||||||
|
return nil, zerrors.ThrowInvalidArgument(nil, "COMMAND-aS3Vz5t6BS", "Errors.IDMissing")
|
||||||
|
}
|
||||||
|
existingUser, err := c.getSchemaUserExists(ctx, resourceOwner, userID)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
if !existingUser.Exists() {
|
||||||
|
return nil, zerrors.ThrowNotFound(nil, "COMMAND-6T2xrOHxTx", "Errors.User.NotFound")
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := c.checkPermissionUpdateUser(ctx, existingUser.ResourceOwner, existingUser.AggregateID); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
existingSchema, err := c.getSchemaWriteModelByID(ctx, "", existingUser.SchemaID)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
if !existingSchema.Exists() {
|
||||||
|
return nil, zerrors.ThrowNotFound(nil, "COMMAND-6T2xrOHxTx", "TODO")
|
||||||
|
}
|
||||||
|
|
||||||
|
//TODO possible authenticators check
|
||||||
|
return existingUser, nil
|
||||||
|
}
|
||||||
|
|
||||||
func existingSchemaUserWithPermission(ctx context.Context, c *Commands, resourceOwner, userID string) (*UserV3WriteModel, error) {
|
func existingSchemaUserWithPermission(ctx context.Context, c *Commands, resourceOwner, userID string) (*UserV3WriteModel, error) {
|
||||||
if userID == "" {
|
if userID == "" {
|
||||||
return nil, zerrors.ThrowInvalidArgument(nil, "COMMAND-aS3Vz5t6BS", "Errors.IDMissing")
|
return nil, zerrors.ThrowInvalidArgument(nil, "COMMAND-aS3Vz5t6BS", "Errors.IDMissing")
|
||||||
|
@ -391,7 +391,7 @@ func TestCommands_DeleteUsername(t *testing.T) {
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"username added, isOrgSpecific, ok",
|
"username removed, isOrgSpecific, ok",
|
||||||
fields{
|
fields{
|
||||||
eventstore: expectEventstore(
|
eventstore: expectEventstore(
|
||||||
filterUsernameExisting(true),
|
filterUsernameExisting(true),
|
||||||
|
Loading…
x
Reference in New Issue
Block a user