feat: User login commands (#1228)

* feat: change login to command side

* feat: change login to command side

* fix: fix push on user

* feat: user command side

* feat: sign out

* feat: command side login

* feat: command side login

* feat: fix register user

* feat: fix register user

* feat: fix web auth n events

* feat: add machine keys

* feat: send codes

* feat: move authrequest to domain

* feat: move authrequest to domain

* feat: webauthn working

* feat: external users

* feat: external users login

* feat: notify users

* fix: tests

* feat: cascade remove user grants on project remove

* fix: webauthn

* fix: pr requests

* fix: register human with member

* fix: fix bugs

* fix: fix bugs
This commit is contained in:
Fabi
2021-02-08 11:30:30 +01:00
committed by GitHub
parent c65331df1a
commit 320679467b
123 changed files with 2949 additions and 1212 deletions
+1 -1
View File
@@ -38,7 +38,7 @@ func writeModelToLoginPolicy(wm *LoginPolicyWriteModel) *domain.LoginPolicy {
ObjectRoot: writeModelToObjectRoot(wm.WriteModel),
AllowUsernamePassword: wm.AllowUserNamePassword,
AllowRegister: wm.AllowRegister,
AllowExternalIdp: wm.AllowExternalIDP,
AllowExternalIDP: wm.AllowExternalIDP,
ForceMFA: wm.ForceMFA,
PasswordlessType: wm.PasswordlessType,
}
@@ -108,7 +108,7 @@ func (wm *IAMIDPOIDCConfigWriteModel) NewChangedEvent(
if userNameMapping.Valid() && wm.UserNameMapping != userNameMapping {
changes = append(changes, idpconfig.ChangeUserNameMapping(userNameMapping))
}
if reflect.DeepEqual(wm.Scopes, scopes) {
if !reflect.DeepEqual(wm.Scopes, scopes) {
changes = append(changes, idpconfig.ChangeScopes(scopes))
}
if len(changes) == 0 {
+2 -2
View File
@@ -44,7 +44,7 @@ func (r *CommandSide) addDefaultLoginPolicy(ctx context.Context, iamAgg *iam_rep
return caos_errs.ThrowAlreadyExists(nil, "IAM-2B0ps", "Errors.IAM.LoginPolicy.AlreadyExists")
}
iamAgg.PushEvents(iam_repo.NewLoginPolicyAddedEvent(ctx, policy.AllowUsernamePassword, policy.AllowRegister, policy.AllowExternalIdp, policy.ForceMFA, policy.PasswordlessType))
iamAgg.PushEvents(iam_repo.NewLoginPolicyAddedEvent(ctx, policy.AllowUsernamePassword, policy.AllowRegister, policy.AllowExternalIDP, policy.ForceMFA, policy.PasswordlessType))
return nil
}
@@ -72,7 +72,7 @@ func (r *CommandSide) changeDefaultLoginPolicy(ctx context.Context, iamAgg *iam_
if existingPolicy.State == domain.PolicyStateUnspecified || existingPolicy.State == domain.PolicyStateRemoved {
return caos_errs.ThrowNotFound(nil, "IAM-M0sif", "Errors.IAM.LoginPolicy.NotFound")
}
changedEvent, hasChanged := existingPolicy.NewChangedEvent(ctx, policy.AllowUsernamePassword, policy.AllowRegister, policy.AllowExternalIdp, policy.ForceMFA, policy.PasswordlessType)
changedEvent, hasChanged := existingPolicy.NewChangedEvent(ctx, policy.AllowUsernamePassword, policy.AllowRegister, policy.AllowExternalIDP, policy.ForceMFA, policy.PasswordlessType)
if !hasChanged {
return caos_errs.ThrowPreconditionFailed(nil, "IAM-5M9vdd", "Errors.IAM.LoginPolicy.NotChanged")
}
+2 -2
View File
@@ -19,7 +19,7 @@ func (r *CommandSide) AddLoginPolicy(ctx context.Context, policy *domain.LoginPo
}
orgAgg := OrgAggregateFromWriteModel(&addedPolicy.WriteModel)
orgAgg.PushEvents(org.NewLoginPolicyAddedEvent(ctx, policy.AllowUsernamePassword, policy.AllowRegister, policy.AllowExternalIdp, policy.ForceMFA, policy.PasswordlessType))
orgAgg.PushEvents(org.NewLoginPolicyAddedEvent(ctx, policy.AllowUsernamePassword, policy.AllowRegister, policy.AllowExternalIDP, policy.ForceMFA, policy.PasswordlessType))
err = r.eventstore.PushAggregate(ctx, addedPolicy, orgAgg)
if err != nil {
@@ -38,7 +38,7 @@ func (r *CommandSide) ChangeLoginPolicy(ctx context.Context, policy *domain.Logi
if existingPolicy.State == domain.PolicyStateUnspecified || existingPolicy.State == domain.PolicyStateRemoved {
return nil, caos_errs.ThrowNotFound(nil, "Org-M0sif", "Errors.Org.LoginPolicy.NotFound")
}
changedEvent, hasChanged := existingPolicy.NewChangedEvent(ctx, policy.AllowUsernamePassword, policy.AllowRegister, policy.AllowExternalIdp, policy.ForceMFA, domain.PasswordlessType(policy.PasswordlessType))
changedEvent, hasChanged := existingPolicy.NewChangedEvent(ctx, policy.AllowUsernamePassword, policy.AllowRegister, policy.AllowExternalIDP, policy.ForceMFA, domain.PasswordlessType(policy.PasswordlessType))
if !hasChanged {
return nil, caos_errs.ThrowPreconditionFailed(nil, "Org-5M9vdd", "Errors.Org.LoginPolicy.NotChanged")
}
+14 -3
View File
@@ -2,6 +2,7 @@ package command
import (
"context"
"github.com/caos/zitadel/internal/eventstore/v2"
caos_errs "github.com/caos/zitadel/internal/errors"
"github.com/caos/zitadel/internal/v2/domain"
@@ -144,7 +145,7 @@ func (r *CommandSide) ReactivateProject(ctx context.Context, projectID string, r
return r.eventstore.PushAggregate(ctx, existingProject, projectAgg)
}
func (r *CommandSide) RemoveProject(ctx context.Context, projectID, resourceOwner string) error {
func (r *CommandSide) RemoveProject(ctx context.Context, projectID, resourceOwner string, cascadingGrantIDs ...string) error {
if projectID == "" || resourceOwner == "" {
return caos_errs.ThrowPreconditionFailed(nil, "COMMAND-66hM9", "Errors.Project.ProjectIDMissing")
}
@@ -157,11 +158,21 @@ func (r *CommandSide) RemoveProject(ctx context.Context, projectID, resourceOwne
return caos_errs.ThrowNotFound(nil, "COMMAND-3M9sd", "Errors.Project.NotFound")
}
aggregates := make([]eventstore.Aggregater, 0)
projectAgg := ProjectAggregateFromWriteModel(&existingProject.WriteModel)
projectAgg.PushEvents(project.NewProjectRemovedEvent(ctx, existingProject.Name, existingProject.ResourceOwner))
//TODO: Remove User Grants by ProjectID
aggregates = append(aggregates, projectAgg)
return r.eventstore.PushAggregate(ctx, existingProject, projectAgg)
for _, grantID := range cascadingGrantIDs {
grantAgg, _, err := r.removeUserGrant(ctx, grantID, "", true)
if err != nil {
return err
}
aggregates = append(aggregates, grantAgg)
}
_, err = r.eventstore.PushAggregates(ctx, aggregates...)
return err
}
func (r *CommandSide) getProjectWriteModelByID(ctx context.Context, projectID, resourceOwner string) (*ProjectWriteModel, error) {
+1 -1
View File
@@ -91,7 +91,7 @@ func (r *CommandSide) SetupStep1(ctx context.Context, step1 *Step1) error {
&domain.LoginPolicy{
AllowUsernamePassword: step1.DefaultLoginPolicy.AllowUsernamePassword,
AllowRegister: step1.DefaultLoginPolicy.AllowRegister,
AllowExternalIdp: step1.DefaultLoginPolicy.AllowExternalIdp,
AllowExternalIDP: step1.DefaultLoginPolicy.AllowExternalIdp,
})
if err != nil {
return err
+69 -2
View File
@@ -2,6 +2,10 @@ package command
import (
"context"
auth_req_model "github.com/caos/zitadel/internal/auth_request/model"
"github.com/caos/zitadel/internal/eventstore/models"
"strings"
"time"
caos_errs "github.com/caos/zitadel/internal/errors"
"github.com/caos/zitadel/internal/telemetry/tracing"
@@ -21,7 +25,7 @@ func (r *CommandSide) ChangeUsername(ctx context.Context, orgID, userID, userNam
return caos_errs.ThrowNotFound(nil, "COMMAND-5N9ds", "Errors.User.NotFound")
}
if existingUser.UserName == userName {
return caos_errs.ThrowPreconditionFailed(nil, "COMMAND-2M9fs", "Errors.User.UsernameNotChanged")
return caos_errs.ThrowPreconditionFailed(nil, "COMMAND-6m9gs", "Errors.User.UsernameNotChanged")
}
orgIAMPolicy, err := r.getOrgIAMPolicy(ctx, orgID)
@@ -89,7 +93,7 @@ func (r *CommandSide) LockUser(ctx context.Context, userID, resourceOwner string
return caos_errs.ThrowNotFound(nil, "COMMAND-5M9fs", "Errors.User.NotFound")
}
if existingUser.UserState != domain.UserStateActive && existingUser.UserState != domain.UserStateInitial {
return caos_errs.ThrowPreconditionFailed(nil, "COMMAND-2M9fs", "Errors.User.ShouldBeActiveOrInitial")
return caos_errs.ThrowPreconditionFailed(nil, "COMMAND-3NN8v", "Errors.User.ShouldBeActiveOrInitial")
}
userAgg := UserAggregateFromWriteModel(&existingUser.WriteModel)
userAgg.PushEvents(user.NewUserLockedEvent(ctx))
@@ -139,6 +143,69 @@ func (r *CommandSide) RemoveUser(ctx context.Context, userID, resourceOwner stri
return r.eventstore.PushAggregate(ctx, existingUser, userAgg)
}
func (r *CommandSide) CreateUserToken(ctx context.Context, orgID, agentID, clientID, userID string, audience, scopes []string, lifetime time.Duration) (*domain.Token, error) {
if orgID == "" || userID == "" {
return nil, caos_errs.ThrowPreconditionFailed(nil, "COMMAND-55n8M", "Errors.IDMissing")
}
existingUser, err := r.userWriteModelByID(ctx, userID, orgID)
if err != nil {
return nil, err
}
if existingUser.UserState == domain.UserStateUnspecified || existingUser.UserState == domain.UserStateDeleted {
return nil, caos_errs.ThrowNotFound(nil, "COMMAND-1d6Gg", "Errors.User.NotFound")
}
for _, scope := range scopes {
if strings.HasPrefix(scope, auth_req_model.ProjectIDScope) && strings.HasSuffix(scope, auth_req_model.AudSuffix) {
audience = append(audience, strings.TrimSuffix(strings.TrimPrefix(scope, auth_req_model.ProjectIDScope), auth_req_model.AudSuffix))
}
}
preferredLanguage := ""
existingHuman, err := r.getHumanWriteModelByID(ctx, userID, orgID)
if existingHuman != nil {
preferredLanguage = existingHuman.PreferredLanguage.String()
}
now := time.Now().UTC()
tokenID, err := r.idGenerator.Next()
if err != nil {
return nil, err
}
userAgg := UserAggregateFromWriteModel(&existingUser.WriteModel)
userAgg.PushEvents(user.NewUserTokenAddedEvent(ctx, tokenID, clientID, agentID, preferredLanguage, audience, scopes, now.Add(lifetime)))
err = r.eventstore.PushAggregate(ctx, existingUser, userAgg)
if err != nil {
return nil, err
}
return &domain.Token{
ObjectRoot: models.ObjectRoot{
AggregateID: userID,
},
TokenID: tokenID,
UserAgentID: agentID,
ApplicationID: clientID,
Audience: audience,
Scopes: scopes,
Expiration: now.Add(lifetime),
PreferredLanguage: preferredLanguage,
}, nil
}
func (r *CommandSide) UserDomainClaimedSent(ctx context.Context, orgID, userID string) (err error) {
existingUser, err := r.userWriteModelByID(ctx, userID, orgID)
if err != nil {
return err
}
if existingUser.UserState == domain.UserStateUnspecified || existingUser.UserState == domain.UserStateDeleted {
return caos_errs.ThrowNotFound(nil, "COMMAND-5m9gK", "Errors.User.NotFound")
}
userAgg := UserAggregateFromWriteModel(&existingUser.WriteModel)
userAgg.PushEvents(user.NewDomainClaimedSentEvent(ctx))
return r.eventstore.PushAggregate(ctx, existingUser, userAgg)
}
func (r *CommandSide) checkUserExists(ctx context.Context, userID, resourceOwner string) error {
userWriteModel, err := r.userWriteModelByID(ctx, userID, resourceOwner)
if err != nil {
+26
View File
@@ -2,6 +2,7 @@ package command
import (
"github.com/caos/zitadel/internal/v2/domain"
"github.com/caos/zitadel/internal/v2/repository/user"
)
func writeModelToHuman(wm *HumanWriteModel) *domain.Human {
@@ -77,6 +78,15 @@ func writeModelToMachine(wm *MachineWriteModel) *domain.Machine {
}
}
func keyWriteModelToMachineKey(wm *MachineKeyWriteModel) *domain.MachineKey {
return &domain.MachineKey{
ObjectRoot: writeModelToObjectRoot(wm.WriteModel),
KeyID: wm.KeyID,
Type: wm.KeyType,
ExpirationDate: wm.ExpirationDate,
}
}
func readModelToU2FTokens(wm *HumanU2FTokensReadModel) []*domain.WebAuthNToken {
tokens := make([]*domain.WebAuthNToken, len(wm.WebAuthNTokens))
for i, token := range wm.WebAuthNTokens {
@@ -107,3 +117,19 @@ func writeModelToWebAuthN(wm *HumanWebAuthNWriteModel) *domain.WebAuthNToken {
State: wm.State,
}
}
func authRequestDomainToAuthRequestInfo(authRequest *domain.AuthRequest) *user.AuthRequestInfo {
info := &user.AuthRequestInfo{
ID: authRequest.ID,
UserAgentID: authRequest.AgentID,
SelectedIDPConfigID: authRequest.SelectedIDPConfigID,
}
if authRequest.BrowserInfo != nil {
info.BrowserInfo = &user.BrowserInfo{
UserAgent: authRequest.BrowserInfo.UserAgent,
AcceptLanguage: authRequest.BrowserInfo.AcceptLanguage,
RemoteIP: authRequest.BrowserInfo.RemoteIP,
}
}
return info
}
+1 -2
View File
@@ -189,7 +189,7 @@ func (r *CommandSide) BulkRemoveUserGrant(ctx context.Context, grantIDs []string
}
func (r *CommandSide) removeUserGrant(ctx context.Context, grantID, resourceOwner string, cascade bool) (_ *usergrant.Aggregate, _ *UserGrantWriteModel, err error) {
if grantID == "" || resourceOwner == "" {
if grantID == "" {
return nil, nil, caos_errs.ThrowPreconditionFailed(nil, "COMMAND-J9sc5", "Errors.UserGrant.IDMissing")
}
@@ -205,7 +205,6 @@ func (r *CommandSide) removeUserGrant(ctx context.Context, grantID, resourceOwne
return nil, nil, caos_errs.ThrowNotFound(nil, "COMMAND-1My0t", "Errors.UserGrant.NotFound")
}
//TODO: Remove Uniqueness
removeUserGrant := NewUserGrantWriteModel(grantID, resourceOwner)
userGrantAgg := UserGrantAggregateFromWriteModel(&removeUserGrant.WriteModel)
if !cascade {
+6 -3
View File
@@ -62,9 +62,12 @@ func (wm *UserGrantWriteModel) Reduce() error {
}
func (wm *UserGrantWriteModel) Query() *eventstore.SearchQueryBuilder {
return eventstore.NewSearchQueryBuilder(eventstore.ColumnsEvent, usergrant.AggregateType).
AggregateIDs(wm.AggregateID).
ResourceOwner(wm.ResourceOwner)
query := eventstore.NewSearchQueryBuilder(eventstore.ColumnsEvent, usergrant.AggregateType).
AggregateIDs(wm.AggregateID)
if wm.ResourceOwner != "" {
query.ResourceOwner(wm.ResourceOwner)
}
return query
}
func UserGrantAggregateFromWriteModel(wm *eventstore.WriteModel) *usergrant.Aggregate {
+59 -34
View File
@@ -2,6 +2,7 @@ package command
import (
"context"
"github.com/caos/zitadel/internal/eventstore/models"
"github.com/caos/zitadel/internal/eventstore/v2"
caos_errs "github.com/caos/zitadel/internal/errors"
@@ -27,9 +28,6 @@ func (r *CommandSide) AddHuman(ctx context.Context, orgID string, human *domain.
}
err = r.eventstore.PushAggregate(ctx, addedHuman, userAgg)
if err != nil {
if caos_errs.IsErrorAlreadyExists(err) {
return nil, caos_errs.ThrowAlreadyExists(err, "COMMAND-4kSff", "Errors.User.AlreadyExists")
}
return nil, err
}
@@ -43,19 +41,36 @@ func (r *CommandSide) addHuman(ctx context.Context, orgID string, human *domain.
return r.createHuman(ctx, orgID, human, nil, false)
}
func (r *CommandSide) RegisterHuman(ctx context.Context, orgID string, human *domain.Human, externalIDP *domain.ExternalIDP) (*domain.Human, error) {
func (r *CommandSide) RegisterHuman(ctx context.Context, orgID string, human *domain.Human, externalIDP *domain.ExternalIDP, orgMemberRoles []string) (*domain.Human, error) {
aggregates := make([]eventstore.Aggregater, 2)
userAgg, addedHuman, err := r.registerHuman(ctx, orgID, human, externalIDP)
if err != nil {
return nil, err
}
err = r.eventstore.PushAggregate(ctx, addedHuman, userAgg)
if err != nil {
if caos_errs.IsErrorAlreadyExists(err) {
return nil, caos_errs.ThrowAlreadyExists(err, "COMMAND-4kSff", "Errors.User.AlreadyExists")
aggregates[0] = userAgg
orgMemberWriteModel := NewOrgMemberWriteModel(orgID, addedHuman.AggregateID)
orgAgg := OrgAggregateFromWriteModel(&orgMemberWriteModel.WriteModel)
if orgMemberRoles != nil {
orgMember := &domain.Member{
ObjectRoot: models.ObjectRoot{
AggregateID: orgID,
},
UserID: userAgg.ID(),
Roles: orgMemberRoles,
}
return nil, err
r.addOrgMember(ctx, orgAgg, orgMemberWriteModel, orgMember)
}
aggregates[1] = orgAgg
eventReader, err := r.eventstore.PushAggregates(ctx, aggregates...)
if err != nil {
return nil, err
}
addedHuman.AppendEvents(eventReader...)
addedHuman.Reduce()
return writeModelToHuman(addedHuman), nil
}
@@ -82,7 +97,7 @@ func (r *CommandSide) createHuman(ctx context.Context, orgID string, human *doma
}
addedHuman := NewHumanWriteModel(human.AggregateID, orgID)
if err := human.CheckOrgIAMPolicy(human.Username, orgIAMPolicy); err != nil {
if err := human.CheckOrgIAMPolicy(orgIAMPolicy); err != nil {
return nil, nil, err
}
human.SetNamesAsDisplayname()
@@ -100,11 +115,10 @@ func (r *CommandSide) createHuman(ctx context.Context, orgID string, human *doma
userAgg.PushEvents(createEvent)
if externalIDP != nil {
if !externalIDP.IsValid() {
return nil, nil, caos_errs.ThrowPreconditionFailed(nil, "COMMAND-4Dj9s", "Errors.User.ExternalIDP.Invalid")
err = r.addHumanExternalIDP(ctx, userAgg, externalIDP)
if err != nil {
return nil, nil, err
}
//TODO: check if idpconfig exists
userAgg.PushEvents(user.NewHumanExternalIDPAddedEvent(ctx, externalIDP.IDPConfigID, externalIDP.DisplayName))
}
if human.IsInitialState() {
initCode, err := domain.NewInitUserCode(r.initializeUserCode)
@@ -121,7 +135,7 @@ func (r *CommandSide) createHuman(ctx context.Context, orgID string, human *doma
if err != nil {
return nil, nil, err
}
user.NewHumanPhoneCodeAddedEvent(ctx, phoneCode.Code, phoneCode.Expiry)
userAgg.PushEvents(user.NewHumanPhoneCodeAddedEvent(ctx, phoneCode.Code, phoneCode.Expiry))
} else if human.Phone != nil && human.PhoneNumber != "" && human.IsPhoneVerified {
userAgg.PushEvents(user.NewHumanPhoneVerifiedEvent(ctx))
}
@@ -129,32 +143,21 @@ func (r *CommandSide) createHuman(ctx context.Context, orgID string, human *doma
return userAgg, addedHuman, nil
}
func (r *CommandSide) ResendInitialMail(ctx context.Context, userID, email, resourceowner string) (err error) {
func (r *CommandSide) HumanSkipMFAInit(ctx context.Context, userID, resourceowner string) (err error) {
if userID == "" {
return caos_errs.ThrowPreconditionFailed(nil, "COMMAND-2M9fs", "Errors.User.UserIDMissing")
return caos_errs.ThrowPreconditionFailed(nil, "COMMAND-2xpX9", "Errors.User.UserIDMissing")
}
existingEmail, err := r.emailWriteModel(ctx, userID, resourceowner)
existingHuman, err := r.getHumanWriteModelByID(ctx, userID, resourceowner)
if err != nil {
return err
}
if existingEmail.UserState == domain.UserStateUnspecified || existingEmail.UserState == domain.UserStateDeleted {
return caos_errs.ThrowNotFound(nil, "COMMAND-2M9df", "Errors.User.NotFound")
if existingHuman.UserState == domain.UserStateUnspecified || existingHuman.UserState == domain.UserStateDeleted {
return caos_errs.ThrowNotFound(nil, "COMMAND-m9cV8", "Errors.User.NotFound")
}
if existingEmail.UserState != domain.UserStateInitial {
return caos_errs.ThrowPreconditionFailed(nil, "COMMAND-2M9sd", "Errors.User.AlreadyInitialised")
}
userAgg := UserAggregateFromWriteModel(&existingEmail.WriteModel)
if email != "" && existingEmail.Email != email {
changedEvent, _ := existingEmail.NewChangedEvent(ctx, email)
userAgg.PushEvents(changedEvent)
}
initCode, err := domain.NewInitUserCode(r.initializeUserCode)
if err != nil {
return err
}
userAgg.PushEvents(user.NewHumanInitialCodeAddedEvent(ctx, initCode.Code, initCode.Expiry))
return r.eventstore.PushAggregate(ctx, existingEmail, userAgg)
userAgg := UserAggregateFromWriteModel(&existingHuman.WriteModel)
userAgg.PushEvents(user.NewHumanMFAInitSkippedEvent(ctx))
return r.eventstore.PushAggregate(ctx, existingHuman, userAgg)
}
func createAddHumanEvent(ctx context.Context, orgID string, human *domain.Human, userLoginMustBeDomain bool) *user.HumanAddedEvent {
@@ -219,6 +222,28 @@ func createRegisterHumanEvent(ctx context.Context, orgID string, human *domain.H
return addEvent
}
func (r *CommandSide) HumansSignOut(ctx context.Context, agentID string, userIDs []string) error {
if agentID == "" {
return caos_errs.ThrowPreconditionFailed(nil, "COMMAND-2M0ds", "Errors.User.UserIDMissing")
}
aggregates := make([]eventstore.Aggregater, len(userIDs))
for i, userID := range userIDs {
existingUser, err := r.getHumanWriteModelByID(ctx, userID, "")
if err != nil {
return err
}
if existingUser.UserState == domain.UserStateUnspecified || existingUser.UserState == domain.UserStateDeleted {
continue
}
userAgg := UserAggregateFromWriteModel(&existingUser.WriteModel)
userAgg.PushEvents(user.NewHumanSignedOutEvent(ctx, agentID))
aggregates[i] = userAgg
}
_, err := r.eventstore.PushAggregates(ctx, aggregates...)
return err
}
func (r *CommandSide) getHumanWriteModelByID(ctx context.Context, userID, resourceowner string) (*HumanWriteModel, error) {
humanWriteModel := NewHumanWriteModel(userID, resourceowner)
err := r.eventstore.FilterToQueryReducer(ctx, humanWriteModel)
+15 -2
View File
@@ -24,7 +24,7 @@ func (r *CommandSide) ChangeHumanEmail(ctx context.Context, email *domain.Email)
}
changedEvent, hasChanged := existingEmail.NewChangedEvent(ctx, email.EmailAddress)
if !hasChanged {
return nil, caos_errs.ThrowPreconditionFailed(nil, "COMMAND-2M9fs", "Errors.User.Email.NotChanged")
return nil, caos_errs.ThrowPreconditionFailed(nil, "COMMAND-2b7fM", "Errors.User.Email.NotChanged")
}
userAgg := UserAggregateFromWriteModel(&existingEmail.WriteModel)
userAgg.PushEvents(changedEvent)
@@ -60,7 +60,7 @@ func (r *CommandSide) VerifyHumanEmail(ctx context.Context, userID, code, resour
return err
}
if existingCode.Code == nil || existingCode.UserState == domain.UserStateUnspecified || existingCode.UserState == domain.UserStateDeleted {
return caos_errs.ThrowNotFound(nil, "COMMAND-2M9fs", "Errors.User.Code.NotFound")
return caos_errs.ThrowNotFound(nil, "COMMAND-3n8ud", "Errors.User.Code.NotFound")
}
userAgg := UserAggregateFromWriteModel(&existingCode.WriteModel)
@@ -103,6 +103,19 @@ func (r *CommandSide) CreateHumanEmailVerificationCode(ctx context.Context, user
return r.eventstore.PushAggregate(ctx, existingEmail, userAgg)
}
func (r *CommandSide) HumanEmailVerificationCodeSent(ctx context.Context, orgID, userID string) (err error) {
existingEmail, err := r.emailWriteModel(ctx, userID, orgID)
if err != nil {
return err
}
if existingEmail.UserState == domain.UserStateUnspecified || existingEmail.UserState == domain.UserStateDeleted {
return caos_errs.ThrowNotFound(nil, "COMMAND-6n8uH", "Errors.User.Email.NotFound")
}
userAgg := UserAggregateFromWriteModel(&existingEmail.WriteModel)
userAgg.PushEvents(user.NewHumanEmailCodeSentEvent(ctx))
return r.eventstore.PushAggregate(ctx, existingEmail, userAgg)
}
func (r *CommandSide) emailWriteModel(ctx context.Context, userID, resourceOwner string) (writeModel *HumanEmailWriteModel, err error) {
ctx, span := tracing.NewSpan(ctx)
defer func() { span.EndWithError(err) }()
@@ -66,9 +66,12 @@ func (wm *HumanEmailWriteModel) Reduce() error {
}
func (wm *HumanEmailWriteModel) Query() *eventstore.SearchQueryBuilder {
return eventstore.NewSearchQueryBuilder(eventstore.ColumnsEvent, user.AggregateType).
AggregateIDs(wm.AggregateID).
ResourceOwner(wm.ResourceOwner)
query := eventstore.NewSearchQueryBuilder(eventstore.ColumnsEvent, user.AggregateType).
AggregateIDs(wm.AggregateID)
if wm.ResourceOwner != "" {
query.ResourceOwner(wm.ResourceOwner)
}
return query
}
func (wm *HumanEmailWriteModel) NewChangedEvent(
@@ -3,11 +3,40 @@ package command
import (
"context"
caos_errs "github.com/caos/zitadel/internal/errors"
"github.com/caos/zitadel/internal/eventstore/v2"
"github.com/caos/zitadel/internal/telemetry/tracing"
"github.com/caos/zitadel/internal/v2/domain"
"github.com/caos/zitadel/internal/v2/repository/user"
)
func (r *CommandSide) BulkAddedHumanExternalIDP(ctx context.Context, userID, resourceOwner string, externalIDPs []*domain.ExternalIDP) error {
if len(externalIDPs) == 0 {
return caos_errs.ThrowPreconditionFailed(nil, "COMMAND-Ek9s", "Errors.User.ExternalIDP.MinimumExternalIDPNeeded")
}
aggregates := make([]eventstore.Aggregater, len(externalIDPs))
for i, externalIDP := range externalIDPs {
externalIDPWriteModel := NewHumanExternalIDPWriteModel(userID, externalIDP.IDPConfigID, externalIDP.ExternalUserID, resourceOwner)
userAgg := UserAggregateFromWriteModel(&externalIDPWriteModel.WriteModel)
err := r.addHumanExternalIDP(ctx, userAgg, externalIDP)
if err != nil {
return err
}
aggregates[i] = userAgg
}
_, err := r.eventstore.PushAggregates(ctx, aggregates...)
return err
}
func (r *CommandSide) addHumanExternalIDP(ctx context.Context, userAgg *user.Aggregate, externalIDP *domain.ExternalIDP) error {
if !externalIDP.IsValid() {
return caos_errs.ThrowPreconditionFailed(nil, "COMMAND-6m9Kd", "Errors.User.ExternalIDP.Invalid")
}
//TODO: check if idpconfig exists
userAgg.PushEvents(user.NewHumanExternalIDPAddedEvent(ctx, externalIDP.IDPConfigID, externalIDP.DisplayName, externalIDP.ExternalUserID))
return nil
}
func (r *CommandSide) RemoveHumanExternalIDP(ctx context.Context, externalIDP *domain.ExternalIDP) error {
return r.removeHumanExternalIDP(ctx, externalIDP, false)
}
@@ -37,6 +66,24 @@ func (r *CommandSide) removeHumanExternalIDP(ctx context.Context, externalIDP *d
return r.eventstore.PushAggregate(ctx, existingExternalIDP, userAgg)
}
func (r *CommandSide) HumanExternalLoginChecked(ctx context.Context, orgID, userID string, authRequest *domain.AuthRequest) (err error) {
if userID == "" {
return caos_errs.ThrowNotFound(nil, "COMMAND-5n8sM", "Errors.IDMissing")
}
existingHuman, err := r.getHumanWriteModelByID(ctx, userID, orgID)
if err != nil {
return err
}
if existingHuman.UserState == domain.UserStateUnspecified || existingHuman.UserState == domain.UserStateDeleted {
return caos_errs.ThrowNotFound(nil, "COMMAND-dn88J", "Errors.User.NotFound")
}
userAgg := UserAggregateFromWriteModel(&existingHuman.WriteModel)
userAgg.PushEvents(user.NewHumanExternalIDPCheckSucceededEvent(ctx, authRequestDomainToAuthRequestInfo(authRequest)))
return r.eventstore.PushAggregate(ctx, existingHuman, userAgg)
}
func (r *CommandSide) externalIDPWriteModelByID(ctx context.Context, userID, idpConfigID, externalUserID, resourceOwner string) (writeModel *HumanExternalIDPWriteModel, err error) {
ctx, span := tracing.NewSpan(ctx)
defer func() { span.EndWithError(err) }()
+104
View File
@@ -0,0 +1,104 @@
package command
import (
"context"
"github.com/caos/logging"
"github.com/caos/zitadel/internal/crypto"
caos_errs "github.com/caos/zitadel/internal/errors"
"github.com/caos/zitadel/internal/v2/domain"
"github.com/caos/zitadel/internal/v2/repository/user"
)
//ResendInitialMail resend inital mail and changes email if provided
func (r *CommandSide) ResendInitialMail(ctx context.Context, userID, email, resourceOwner string) (err error) {
if userID == "" {
return caos_errs.ThrowPreconditionFailed(nil, "COMMAND-2n8vs", "Errors.User.UserIDMissing")
}
existingCode, err := r.getHumanInitWriteModelByID(ctx, userID, resourceOwner)
if err != nil {
return err
}
if existingCode.UserState == domain.UserStateUnspecified || existingCode.UserState == domain.UserStateDeleted {
return caos_errs.ThrowNotFound(nil, "COMMAND-2M9df", "Errors.User.NotFound")
}
if existingCode.UserState != domain.UserStateInitial {
return caos_errs.ThrowPreconditionFailed(nil, "COMMAND-2M9sd", "Errors.User.AlreadyInitialised")
}
userAgg := UserAggregateFromWriteModel(&existingCode.WriteModel)
if email != "" && existingCode.Email != email {
changedEvent, _ := existingCode.NewChangedEvent(ctx, email)
userAgg.PushEvents(changedEvent)
}
initCode, err := domain.NewInitUserCode(r.initializeUserCode)
if err != nil {
return err
}
userAgg.PushEvents(user.NewHumanInitialCodeAddedEvent(ctx, initCode.Code, initCode.Expiry))
return r.eventstore.PushAggregate(ctx, existingCode, userAgg)
}
func (r *CommandSide) HumanVerifyInitCode(ctx context.Context, userID, resourceOwner, code, passwordString string) error {
if userID == "" {
return caos_errs.ThrowPreconditionFailed(nil, "COMMAND-mkM9f", "Errors.User.UserIDMissing")
}
if code == "" {
return caos_errs.ThrowPreconditionFailed(nil, "COMMAND-44G8s", "Errors.User.Code.Empty")
}
existingCode, err := r.getHumanInitWriteModelByID(ctx, userID, resourceOwner)
if err != nil {
return err
}
if existingCode.Code == nil || existingCode.UserState == domain.UserStateUnspecified || existingCode.UserState == domain.UserStateDeleted {
return caos_errs.ThrowNotFound(nil, "COMMAND-mmn5f", "Errors.User.Code.NotFound")
}
userAgg := UserAggregateFromWriteModel(&existingCode.WriteModel)
err = crypto.VerifyCode(existingCode.CodeCreationDate, existingCode.CodeExpiry, existingCode.Code, code, r.initializeUserCode)
if err != nil {
userAgg.PushEvents(user.NewHumanInitializedCheckFailedEvent(ctx))
err = r.eventstore.PushAggregate(ctx, existingCode, userAgg)
logging.LogWithFields("COMMAND-Dg2z5", "userID", userAgg.ID()).OnError(err).Error("NewHumanInitializedCheckFailedEvent push failed")
return caos_errs.ThrowInvalidArgument(err, "COMMAND-11v6G", "Errors.User.Code.Invalid")
}
userAgg.PushEvents(user.NewHumanInitializedCheckSucceededEvent(ctx))
if !existingCode.IsEmailVerified {
userAgg.PushEvents(user.NewHumanEmailVerifiedEvent(ctx))
}
if passwordString != "" {
passwordWriteModel := NewHumanPasswordWriteModel(userID, existingCode.ResourceOwner)
password := &domain.Password{
SecretString: passwordString,
ChangeRequired: false,
}
err = r.changePassword(ctx, existingCode.ResourceOwner, userID, "", password, userAgg, passwordWriteModel)
if err != nil {
return err
}
}
return r.eventstore.PushAggregate(ctx, existingCode, userAgg)
}
func (r *CommandSide) HumanInitCodeSent(ctx context.Context, orgID, userID string) (err error) {
existingInitCode, err := r.getHumanInitWriteModelByID(ctx, userID, orgID)
if err != nil {
return err
}
if existingInitCode.UserState == domain.UserStateUnspecified || existingInitCode.UserState == domain.UserStateDeleted {
return caos_errs.ThrowNotFound(nil, "COMMAND-556zg", "Errors.User.Code.NotFound")
}
userAgg := UserAggregateFromWriteModel(&existingInitCode.WriteModel)
userAgg.PushEvents(user.NewHumanInitialCodeSentEvent(ctx))
return r.eventstore.PushAggregate(ctx, existingInitCode, userAgg)
}
func (r *CommandSide) getHumanInitWriteModelByID(ctx context.Context, userID, resourceowner string) (*HumanInitCodeWriteModel, error) {
initWriteModel := NewHumanInitCodeWriteModel(userID, resourceowner)
err := r.eventstore.FilterToQueryReducer(ctx, initWriteModel)
if err != nil {
return nil, err
}
return initWriteModel, nil
}
@@ -0,0 +1,89 @@
package command
import (
"context"
"github.com/caos/zitadel/internal/crypto"
"github.com/caos/zitadel/internal/eventstore/v2"
"github.com/caos/zitadel/internal/v2/domain"
"github.com/caos/zitadel/internal/v2/repository/user"
"time"
)
type HumanInitCodeWriteModel struct {
eventstore.WriteModel
Email string
IsEmailVerified bool
Code *crypto.CryptoValue
CodeCreationDate time.Time
CodeExpiry time.Duration
UserState domain.UserState
}
func NewHumanInitCodeWriteModel(userID, resourceOwner string) *HumanInitCodeWriteModel {
return &HumanInitCodeWriteModel{
WriteModel: eventstore.WriteModel{
AggregateID: userID,
ResourceOwner: resourceOwner,
},
}
}
func (wm *HumanInitCodeWriteModel) AppendEvents(events ...eventstore.EventReader) {
wm.WriteModel.AppendEvents(events...)
}
func (wm *HumanInitCodeWriteModel) Reduce() error {
for _, event := range wm.Events {
switch e := event.(type) {
case *user.HumanAddedEvent:
wm.Email = e.EmailAddress
wm.UserState = domain.UserStateInitial
case *user.HumanRegisteredEvent:
wm.Email = e.EmailAddress
wm.UserState = domain.UserStateInitial
case *user.HumanEmailChangedEvent:
wm.Email = e.EmailAddress
wm.IsEmailVerified = false
case *user.HumanEmailVerifiedEvent:
wm.IsEmailVerified = true
wm.Code = nil
if wm.UserState == domain.UserStateInitial {
wm.UserState = domain.UserStateActive
}
case *user.HumanInitialCodeAddedEvent:
wm.Code = e.Code
wm.CodeCreationDate = e.CreationDate()
wm.CodeExpiry = e.Expiry
case *user.HumanInitializedCheckSucceededEvent:
wm.Code = nil
case *user.UserRemovedEvent:
wm.UserState = domain.UserStateDeleted
}
}
return wm.WriteModel.Reduce()
}
func (wm *HumanInitCodeWriteModel) Query() *eventstore.SearchQueryBuilder {
query := eventstore.NewSearchQueryBuilder(eventstore.ColumnsEvent, user.AggregateType).
AggregateIDs(wm.AggregateID)
if wm.ResourceOwner != "" {
query.ResourceOwner(wm.ResourceOwner)
}
return query
}
func (wm *HumanInitCodeWriteModel) NewChangedEvent(
ctx context.Context,
email string,
) (*user.HumanEmailChangedEvent, bool) {
hasChanged := false
changedEvent := user.NewHumanEmailChangedEvent(ctx)
if wm.Email != email {
hasChanged = true
changedEvent.EmailAddress = email
}
return changedEvent, hasChanged
}
+31 -2
View File
@@ -2,6 +2,7 @@ package command
import (
"context"
"github.com/caos/logging"
caos_errs "github.com/caos/zitadel/internal/errors"
"github.com/caos/zitadel/internal/eventstore/models"
"github.com/caos/zitadel/internal/telemetry/tracing"
@@ -15,14 +16,17 @@ func (r *CommandSide) AddHumanOTP(ctx context.Context, userID, resourceowner str
}
human, err := r.getHuman(ctx, userID, resourceowner)
if err != nil {
logging.Log("COMMAND-DAqe1").WithError(err).WithField("traceID", tracing.TraceIDFromCtx(ctx)).Debug("unable to get human for loginname")
return nil, err
}
org, err := r.getOrg(ctx, human.ResourceOwner)
if err != nil {
logging.Log("COMMAND-Cm0ds").WithError(err).WithField("traceID", tracing.TraceIDFromCtx(ctx)).Debug("unable to get org for loginname")
return nil, err
}
orgPolicy, err := r.getOrgIAMPolicy(ctx, org.AggregateID)
if err != nil {
logging.Log("COMMAND-y5zv9").WithError(err).WithField("traceID", tracing.TraceIDFromCtx(ctx)).Debug("unable to get org policy for loginname")
return nil, err
}
otpWriteModel, err := r.otpWriteModelByID(ctx, userID, resourceowner)
@@ -58,7 +62,7 @@ func (r *CommandSide) AddHumanOTP(ctx context.Context, userID, resourceowner str
}, nil
}
func (r *CommandSide) CheckMFAOTPSetup(ctx context.Context, userID, code, userAgentID, resourceowner string) error {
func (r *CommandSide) HumanCheckMFAOTPSetup(ctx context.Context, userID, code, userAgentID, resourceowner string) error {
if userID == "" {
return caos_errs.ThrowPreconditionFailed(nil, "COMMAND-8N9ds", "Errors.User.UserIDMissing")
}
@@ -84,7 +88,32 @@ func (r *CommandSide) CheckMFAOTPSetup(ctx context.Context, userID, code, userAg
return r.eventstore.PushAggregate(ctx, existingOTP, userAgg)
}
func (r *CommandSide) RemoveHumanOTP(ctx context.Context, userID, resourceOwner string) error {
func (r *CommandSide) HumanCheckMFAOTP(ctx context.Context, userID, code, resourceowner string, authRequest *domain.AuthRequest) error {
if userID == "" {
return caos_errs.ThrowPreconditionFailed(nil, "COMMAND-8N9ds", "Errors.User.UserIDMissing")
}
existingOTP, err := r.otpWriteModelByID(ctx, userID, resourceowner)
if err != nil {
return err
}
if existingOTP.State != domain.MFAStateReady {
return caos_errs.ThrowPreconditionFailed(nil, "COMMAND-3Mif9s", "Errors.User.MFA.OTP.NotReady")
}
userAgg := UserAggregateFromWriteModel(&existingOTP.WriteModel)
err = domain.VerifyMFAOTP(code, existingOTP.Secret, r.multifactors.OTP.CryptoMFA)
if err == nil {
userAgg.PushEvents(
user.NewHumanOTPCheckSucceededEvent(ctx, authRequestDomainToAuthRequestInfo(authRequest)),
)
return r.eventstore.PushAggregate(ctx, existingOTP, userAgg)
}
userAgg.PushEvents(user.NewHumanOTPCheckFailedEvent(ctx, authRequestDomainToAuthRequestInfo(authRequest)))
pushErr := r.eventstore.PushAggregate(ctx, existingOTP, userAgg)
logging.Log("COMMAND-9fj7s").OnError(pushErr).Error("error create password check failed event")
return err
}
func (r *CommandSide) HumanRemoveOTP(ctx context.Context, userID, resourceOwner string) error {
if userID == "" {
return caos_errs.ThrowPreconditionFailed(nil, "COMMAND-5M0sd", "Errors.User.UserIDMissing")
}
+6 -3
View File
@@ -45,7 +45,10 @@ func (wm *HumanOTPWriteModel) Reduce() error {
}
func (wm *HumanOTPWriteModel) Query() *eventstore.SearchQueryBuilder {
return eventstore.NewSearchQueryBuilder(eventstore.ColumnsEvent, user.AggregateType).
AggregateIDs(wm.AggregateID).
ResourceOwner(wm.ResourceOwner)
query := eventstore.NewSearchQueryBuilder(eventstore.ColumnsEvent, user.AggregateType).
AggregateIDs(wm.AggregateID)
if wm.ResourceOwner != "" {
query.ResourceOwner(wm.ResourceOwner)
}
return query
}
+83 -6
View File
@@ -2,6 +2,7 @@ package command
import (
"context"
"github.com/caos/logging"
"github.com/caos/zitadel/internal/crypto"
caos_errs "github.com/caos/zitadel/internal/errors"
"github.com/caos/zitadel/internal/telemetry/tracing"
@@ -21,7 +22,34 @@ func (r *CommandSide) SetOneTimePassword(ctx context.Context, orgID, userID, pas
SecretString: passwordString,
ChangeRequired: true,
}
return r.changePassword(ctx, orgID, userID, "", password, existingPassword)
userAgg := UserAggregateFromWriteModel(&existingPassword.WriteModel)
return r.changePassword(ctx, orgID, userID, "", password, userAgg, existingPassword)
}
func (r *CommandSide) SetPassword(ctx context.Context, orgID, userID, code, passwordString, userAgentID string) (err error) {
ctx, span := tracing.NewSpan(ctx)
defer func() { span.EndWithError(err) }()
existingCode, err := r.passwordWriteModel(ctx, userID, orgID)
if err != nil {
return err
}
if existingCode.Code == nil || existingCode.UserState == domain.UserStateUnspecified || existingCode.UserState == domain.UserStateDeleted {
return caos_errs.ThrowNotFound(nil, "COMMAND-2M9fs", "Errors.User.Code.NotFound")
}
err = crypto.VerifyCode(existingCode.CodeCreationDate, existingCode.CodeExpiry, existingCode.Code, code, r.emailVerificationCode)
if err != nil {
return err
}
password := &domain.Password{
SecretString: passwordString,
ChangeRequired: false,
}
userAgg := UserAggregateFromWriteModel(&existingCode.WriteModel)
return r.changePassword(ctx, existingCode.ResourceOwner, userID, userAgentID, password, userAgg, existingCode)
}
func (r *CommandSide) ChangePassword(ctx context.Context, orgID, userID, oldPassword, newPassword, userAgentID string) (err error) {
@@ -32,7 +60,7 @@ func (r *CommandSide) ChangePassword(ctx context.Context, orgID, userID, oldPass
if err != nil {
return err
}
if existingPassword.Secret != nil {
if existingPassword.Secret == nil {
return caos_errs.ThrowPreconditionFailed(nil, "COMMAND-Fds3s", "Errors.User.Password.Empty")
}
ctx, spanPasswordComparison := tracing.NewNamedSpan(ctx, "crypto.CompareHash")
@@ -44,12 +72,14 @@ func (r *CommandSide) ChangePassword(ctx context.Context, orgID, userID, oldPass
}
password := &domain.Password{
SecretString: newPassword,
ChangeRequired: true,
ChangeRequired: false,
}
return r.changePassword(ctx, orgID, userID, userAgentID, password, existingPassword)
userAgg := UserAggregateFromWriteModel(&existingPassword.WriteModel)
return r.changePassword(ctx, orgID, userID, userAgentID, password, userAgg, existingPassword)
}
func (r *CommandSide) changePassword(ctx context.Context, orgID, userID, userAgentID string, password *domain.Password, existingPassword *HumanPasswordWriteModel) (err error) {
func (r *CommandSide) changePassword(ctx context.Context, orgID, userID, userAgentID string, password *domain.Password, userAgg *user.Aggregate, existingPassword *HumanPasswordWriteModel) (err error) {
ctx, span := tracing.NewSpan(ctx)
defer func() { span.EndWithError(err) }()
@@ -69,7 +99,6 @@ func (r *CommandSide) changePassword(ctx context.Context, orgID, userID, userAge
if err := password.HashPasswordIfExisting(pwPolicy, r.userPasswordAlg); err != nil {
return err
}
userAgg := UserAggregateFromWriteModel(&existingPassword.WriteModel)
userAgg.PushEvents(user.NewHumanPasswordChangedEvent(ctx, password.SecretCrypto, password.ChangeRequired, userAgentID))
return r.eventstore.PushAggregate(ctx, existingPassword, userAgg)
}
@@ -94,6 +123,54 @@ func (r *CommandSide) RequestSetPassword(ctx context.Context, userID, resourceOw
return r.eventstore.PushAggregate(ctx, existingHuman, userAgg)
}
func (r *CommandSide) PasswordCodeSent(ctx context.Context, orgID, userID string) (err error) {
existingPassword, err := r.passwordWriteModel(ctx, userID, orgID)
if err != nil {
return err
}
if existingPassword.UserState == domain.UserStateUnspecified || existingPassword.UserState == domain.UserStateDeleted {
return caos_errs.ThrowNotFound(nil, "COMMAND-3n77z", "Errors.User.NotFound")
}
userAgg := UserAggregateFromWriteModel(&existingPassword.WriteModel)
userAgg.PushEvents(user.NewHumanPasswordCodeSentEvent(ctx))
return r.eventstore.PushAggregate(ctx, existingPassword, userAgg)
}
func (r *CommandSide) HumanCheckPassword(ctx context.Context, orgID, userID, password string, authRequest *domain.AuthRequest) (err error) {
ctx, span := tracing.NewSpan(ctx)
defer func() { span.EndWithError(err) }()
if password == "" {
return caos_errs.ThrowNotFound(nil, "COMMAND-3n8fs", "Errors.User.Password.Empty")
}
existingPassword, err := r.passwordWriteModel(ctx, userID, orgID)
if err != nil {
return err
}
if existingPassword.UserState == domain.UserStateUnspecified || existingPassword.UserState == domain.UserStateDeleted {
return caos_errs.ThrowNotFound(nil, "COMMAND-3n77z", "Errors.User.NotFound")
}
if existingPassword.Secret == nil {
return caos_errs.ThrowNotFound(nil, "COMMAND-3n77z", "Errors.User.Password.NotSet")
}
userAgg := UserAggregateFromWriteModel(&existingPassword.WriteModel)
ctx, spanPasswordComparison := tracing.NewNamedSpan(ctx, "crypto.CompareHash")
err = crypto.CompareHash(existingPassword.Secret, []byte(password), r.userPasswordAlg)
spanPasswordComparison.EndWithError(err)
if err == nil {
userAgg.PushEvents(user.NewHumanPasswordCheckSucceededEvent(ctx, authRequestDomainToAuthRequestInfo(authRequest)))
return r.eventstore.PushAggregate(ctx, existingPassword, userAgg)
}
userAgg.PushEvents(user.NewHumanPasswordCheckFailedEvent(ctx, authRequestDomainToAuthRequestInfo(authRequest)))
err = r.eventstore.PushAggregate(ctx, existingPassword, userAgg)
logging.Log("COMMAND-9fj7s").OnError(err).Error("error create password check failed event")
return caos_errs.ThrowInvalidArgument(nil, "COMMAND-452ad", "Errors.User.Password.Invalid")
}
func (r *CommandSide) passwordWriteModel(ctx context.Context, userID, resourceOwner string) (writeModel *HumanPasswordWriteModel, err error) {
ctx, span := tracing.NewSpan(ctx)
defer func() { span.EndWithError(err) }()
@@ -5,6 +5,7 @@ import (
"github.com/caos/zitadel/internal/eventstore/v2"
"github.com/caos/zitadel/internal/v2/domain"
"github.com/caos/zitadel/internal/v2/repository/user"
"time"
)
type HumanPasswordWriteModel struct {
@@ -13,6 +14,10 @@ type HumanPasswordWriteModel struct {
Secret *crypto.CryptoValue
SecretChangeRequired bool
Code *crypto.CryptoValue
CodeCreationDate time.Time
CodeExpiry time.Duration
UserState domain.UserState
}
@@ -43,6 +48,11 @@ func (wm *HumanPasswordWriteModel) Reduce() error {
case *user.HumanPasswordChangedEvent:
wm.Secret = e.Secret
wm.SecretChangeRequired = e.ChangeRequired
wm.Code = nil
case *user.HumanPasswordCodeAddedEvent:
wm.Code = e.Code
wm.CodeCreationDate = e.CreationDate()
wm.CodeExpiry = e.Expiry
case *user.HumanEmailVerifiedEvent:
if wm.UserState == domain.UserStateInitial {
wm.UserState = domain.UserStateActive
@@ -55,7 +65,10 @@ func (wm *HumanPasswordWriteModel) Reduce() error {
}
func (wm *HumanPasswordWriteModel) Query() *eventstore.SearchQueryBuilder {
return eventstore.NewSearchQueryBuilder(eventstore.ColumnsEvent, user.AggregateType).
AggregateIDs(wm.AggregateID).
ResourceOwner(wm.ResourceOwner)
query := eventstore.NewSearchQueryBuilder(eventstore.ColumnsEvent, user.AggregateType).
AggregateIDs(wm.AggregateID)
if wm.ResourceOwner != "" {
query.ResourceOwner(wm.ResourceOwner)
}
return query
}
+14 -1
View File
@@ -93,7 +93,7 @@ func (r *CommandSide) CreateHumanPhoneVerificationCode(ctx context.Context, user
return err
}
if existingPhone.State == domain.PhoneStateUnspecified || existingPhone.State == domain.PhoneStateRemoved {
return caos_errs.ThrowNotFound(nil, "COMMAND-2M9fs", "Errors.User.Phone.NotFound")
return caos_errs.ThrowNotFound(nil, "COMMAND-2b7Hf", "Errors.User.Phone.NotFound")
}
if existingPhone.IsPhoneVerified {
return caos_errs.ThrowPreconditionFailed(nil, "COMMAND-2M9sf", "Errors.User.Phone.AlreadyVerified")
@@ -107,6 +107,19 @@ func (r *CommandSide) CreateHumanPhoneVerificationCode(ctx context.Context, user
return r.eventstore.PushAggregate(ctx, existingPhone, userAgg)
}
func (r *CommandSide) HumanPhoneVerificationCodeSent(ctx context.Context, orgID, userID string) (err error) {
existingPhone, err := r.phoneWriteModelByID(ctx, userID, orgID)
if err != nil {
return err
}
if existingPhone.State == domain.PhoneStateUnspecified || existingPhone.State == domain.PhoneStateRemoved {
return caos_errs.ThrowNotFound(nil, "COMMAND-66n8J", "Errors.User.Phone.NotFound")
}
userAgg := UserAggregateFromWriteModel(&existingPhone.WriteModel)
userAgg.PushEvents(user.NewHumanPhoneCodeSentEvent(ctx))
return r.eventstore.PushAggregate(ctx, existingPhone, userAgg)
}
func (r *CommandSide) RemoveHumanPhone(ctx context.Context, userID, resourceOwner string) error {
if userID == "" {
return caos_errs.ThrowPreconditionFailed(nil, "COMMAND-6M0ds", "Errors.User.UserIDMissing")
+180 -8
View File
@@ -33,7 +33,35 @@ func (r *CommandSide) getHumanPasswordlessTokens(ctx context.Context, userID, re
return readModelToPasswordlessTokens(tokenReadModel), nil
}
func (r *CommandSide) AddHumanU2F(ctx context.Context, userID, resourceowner string, isLoginUI bool) (*domain.WebAuthNToken, error) {
func (r *CommandSide) getHumanU2FLogin(ctx context.Context, userID, authReqID, resourceowner string) (*domain.WebAuthNLogin, error) {
tokenReadModel := NewHumanU2FLoginReadModel(userID, authReqID, resourceowner)
err := r.eventstore.FilterToQueryReducer(ctx, tokenReadModel)
if err != nil {
return nil, err
}
if tokenReadModel.State == domain.UserStateDeleted {
return nil, caos_errs.ThrowNotFound(nil, "COMMAND-5m88U", "Errors.User.NotFound")
}
return &domain.WebAuthNLogin{
Challenge: tokenReadModel.Challenge,
}, nil
}
func (r *CommandSide) getHumanPasswordlessLogin(ctx context.Context, userID, authReqID, resourceowner string) (*domain.WebAuthNLogin, error) {
tokenReadModel := NewHumanPasswordlessLoginReadModel(userID, authReqID, resourceowner)
err := r.eventstore.FilterToQueryReducer(ctx, tokenReadModel)
if err != nil {
return nil, err
}
if tokenReadModel.State == domain.UserStateDeleted {
return nil, caos_errs.ThrowNotFound(nil, "COMMAND-fm84R", "Errors.User.NotFound")
}
return &domain.WebAuthNLogin{
Challenge: tokenReadModel.Challenge,
}, nil
}
func (r *CommandSide) HumanAddU2FSetup(ctx context.Context, userID, resourceowner string, isLoginUI bool) (*domain.WebAuthNToken, error) {
u2fTokens, err := r.getHumanU2FTokens(ctx, userID, resourceowner)
if err != nil {
return nil, err
@@ -55,7 +83,7 @@ func (r *CommandSide) AddHumanU2F(ctx context.Context, userID, resourceowner str
return createdWebAuthN, nil
}
func (r *CommandSide) AddHumanPasswordless(ctx context.Context, userID, resourceowner string, isLoginUI bool) (*domain.WebAuthNToken, error) {
func (r *CommandSide) HumanAddPasswordlessSetup(ctx context.Context, userID, resourceowner string, isLoginUI bool) (*domain.WebAuthNToken, error) {
passwordlessTokens, err := r.getHumanPasswordlessTokens(ctx, userID, resourceowner)
if err != nil {
return nil, err
@@ -64,7 +92,7 @@ func (r *CommandSide) AddHumanPasswordless(ctx context.Context, userID, resource
if err != nil {
return nil, err
}
userAgg.PushEvents(usr_repo.NewHumanU2FAddedEvent(ctx, addWebAuthN.WebauthNTokenID, webAuthN.Challenge))
userAgg.PushEvents(usr_repo.NewHumanPasswordlessAddedEvent(ctx, addWebAuthN.WebauthNTokenID, webAuthN.Challenge))
err = r.eventstore.PushAggregate(ctx, addWebAuthN, userAgg)
if err != nil {
@@ -114,7 +142,7 @@ func (r *CommandSide) addHumanWebAuthN(ctx context.Context, userID, resourceowne
return addWebAuthN, userAgg, webAuthN, nil
}
func (r *CommandSide) VerifyHumanU2F(ctx context.Context, userID, resourceowner, tokenName, userAgentID string, credentialData []byte) error {
func (r *CommandSide) HumanVerifyU2FSetup(ctx context.Context, userID, resourceowner, tokenName, userAgentID string, credentialData []byte) error {
u2fTokens, err := r.getHumanU2FTokens(ctx, userID, resourceowner)
if err != nil {
return err
@@ -139,7 +167,7 @@ func (r *CommandSide) VerifyHumanU2F(ctx context.Context, userID, resourceowner,
return r.eventstore.PushAggregate(ctx, verifyWebAuthN, userAgg)
}
func (r *CommandSide) VerifyHumanPasswordless(ctx context.Context, userID, resourceowner, tokenName, userAgentID string, credentialData []byte) error {
func (r *CommandSide) HumanHumanPasswordlessSetup(ctx context.Context, userID, resourceowner, tokenName, userAgentID string, credentialData []byte) error {
u2fTokens, err := r.getHumanPasswordlessTokens(ctx, userID, resourceowner)
if err != nil {
return err
@@ -149,7 +177,7 @@ func (r *CommandSide) VerifyHumanPasswordless(ctx context.Context, userID, resou
return err
}
userAgg.PushEvents(
usr_repo.NewHumanU2FVerifiedEvent(
usr_repo.NewHumanPasswordlessVerifiedEvent(
ctx,
verifyWebAuthN.WebauthNTokenID,
webAuthN.WebAuthNTokenName,
@@ -186,12 +214,156 @@ func (r *CommandSide) verifyHumanWebAuthN(ctx context.Context, userID, resourceo
return verifyWebAuthN, userAgg, webAuthN, nil
}
func (r *CommandSide) RemoveHumanU2F(ctx context.Context, userID, webAuthNID, resourceOwner string) error {
func (r *CommandSide) HumanBeginU2FLogin(ctx context.Context, userID, resourceOwner string, authRequest *domain.AuthRequest, isLoginUI bool) (*domain.WebAuthNLogin, error) {
u2fTokens, err := r.getHumanU2FTokens(ctx, userID, resourceOwner)
if err != nil {
return nil, err
}
writeModel, userAgg, webAuthNLogin, err := r.beginWebAuthNLogin(ctx, userID, resourceOwner, u2fTokens, isLoginUI)
if err != nil {
return nil, err
}
userAgg.PushEvents(
usr_repo.NewHumanU2FBeginLoginEvent(
ctx,
webAuthNLogin.Challenge,
authRequestDomainToAuthRequestInfo(authRequest),
),
)
err = r.eventstore.PushAggregate(ctx, writeModel, userAgg)
return webAuthNLogin, err
}
func (r *CommandSide) HumanBeginPasswordlessLogin(ctx context.Context, userID, resourceOwner string, authRequest *domain.AuthRequest, isLoginUI bool) (*domain.WebAuthNLogin, error) {
u2fTokens, err := r.getHumanPasswordlessTokens(ctx, userID, resourceOwner)
if err != nil {
return nil, err
}
writeModel, userAgg, webAuthNLogin, err := r.beginWebAuthNLogin(ctx, userID, resourceOwner, u2fTokens, isLoginUI)
if err != nil {
return nil, err
}
userAgg.PushEvents(
usr_repo.NewHumanPasswordlessBeginLoginEvent(
ctx,
webAuthNLogin.Challenge,
authRequestDomainToAuthRequestInfo(authRequest),
),
)
err = r.eventstore.PushAggregate(ctx, writeModel, userAgg)
return webAuthNLogin, err
}
func (r *CommandSide) beginWebAuthNLogin(ctx context.Context, userID, resourceOwner string, tokens []*domain.WebAuthNToken, isLoginUI bool) (*HumanWebAuthNWriteModel, *usr_repo.Aggregate, *domain.WebAuthNLogin, error) {
if userID == "" {
return nil, nil, nil, caos_errs.ThrowPreconditionFailed(nil, "COMMAND-hh8K9", "Errors.IDMissing")
}
human, err := r.getHuman(ctx, userID, resourceOwner)
if err != nil {
return nil, nil, nil, err
}
webAuthNLogin, err := r.webauthn.BeginLogin(human, domain.UserVerificationRequirementDiscouraged, isLoginUI, tokens...)
if err != nil {
return nil, nil, nil, err
}
writeModel, err := r.webauthNWriteModelByID(ctx, userID, "", resourceOwner)
if err != nil {
return nil, nil, nil, err
}
userAgg := UserAggregateFromWriteModel(&writeModel.WriteModel)
return writeModel, userAgg, webAuthNLogin, nil
}
func (r *CommandSide) HumanFinishU2FLogin(ctx context.Context, userID, resourceOwner string, credentialData []byte, authRequest *domain.AuthRequest, isLoginUI bool) error {
webAuthNLogin, err := r.getHumanU2FLogin(ctx, userID, authRequest.ID, resourceOwner)
if err != nil {
return err
}
u2fTokens, err := r.getHumanU2FTokens(ctx, userID, resourceOwner)
if err != nil {
return err
}
writeModel, userAgg, token, signCount, err := r.finishWebAuthNLogin(ctx, userID, resourceOwner, credentialData, webAuthNLogin, u2fTokens, isLoginUI)
if err != nil {
return err
}
userAgg.PushEvents(
usr_repo.NewHumanU2FSignCountChangedEvent(
ctx,
token.WebAuthNTokenID,
signCount,
),
)
return r.eventstore.PushAggregate(ctx, writeModel, userAgg)
}
func (r *CommandSide) HumanFinishPasswordlessLogin(ctx context.Context, userID, resourceOwner string, credentialData []byte, authRequest *domain.AuthRequest, isLoginUI bool) error {
webAuthNLogin, err := r.getHumanPasswordlessLogin(ctx, userID, authRequest.ID, resourceOwner)
if err != nil {
return err
}
passwordlessTokens, err := r.getHumanPasswordlessTokens(ctx, userID, resourceOwner)
if err != nil {
return err
}
writeModel, userAgg, token, signCount, err := r.finishWebAuthNLogin(ctx, userID, resourceOwner, credentialData, webAuthNLogin, passwordlessTokens, isLoginUI)
if err != nil {
return err
}
userAgg.PushEvents(
usr_repo.NewHumanPasswordlessSignCountChangedEvent(
ctx,
token.WebAuthNTokenID,
signCount,
),
)
return r.eventstore.PushAggregate(ctx, writeModel, userAgg)
}
func (r *CommandSide) finishWebAuthNLogin(ctx context.Context, userID, resourceOwner string, credentialData []byte, webAuthN *domain.WebAuthNLogin, tokens []*domain.WebAuthNToken, isLoginUI bool) (*HumanWebAuthNWriteModel, *usr_repo.Aggregate, *domain.WebAuthNToken, uint32, error) {
if userID == "" {
return nil, nil, nil, 0, caos_errs.ThrowPreconditionFailed(nil, "COMMAND-hh8K9", "Errors.IDMissing")
}
human, err := r.getHuman(ctx, userID, resourceOwner)
if err != nil {
return nil, nil, nil, 0, err
}
keyID, signCount, err := r.webauthn.FinishLogin(human, webAuthN, credentialData, isLoginUI, tokens...)
if err != nil && keyID == nil {
return nil, nil, nil, 0, err
}
_, token := domain.GetTokenByKeyID(tokens, keyID)
if token == nil {
return nil, nil, nil, 0, caos_errs.ThrowPreconditionFailed(nil, "COMMAND-3b7zs", "Errors.User.WebAuthN.NotFound")
}
writeModel, err := r.webauthNWriteModelByID(ctx, userID, "", resourceOwner)
if err != nil {
return nil, nil, nil, 0, err
}
userAgg := UserAggregateFromWriteModel(&writeModel.WriteModel)
return writeModel, userAgg, token, signCount, nil
}
func (r *CommandSide) HumanRemoveU2F(ctx context.Context, userID, webAuthNID, resourceOwner string) error {
event := usr_repo.NewHumanU2FRemovedEvent(ctx, webAuthNID)
return r.removeHumanWebAuthN(ctx, userID, webAuthNID, resourceOwner, event)
}
func (r *CommandSide) RemoveHumanPasswordless(ctx context.Context, userID, webAuthNID, resourceOwner string) error {
func (r *CommandSide) HumanRemovePasswordless(ctx context.Context, userID, webAuthNID, resourceOwner string) error {
event := usr_repo.NewHumanPasswordlessRemovedEvent(ctx, webAuthNID)
return r.removeHumanWebAuthN(ctx, userID, webAuthNID, resourceOwner, event)
}
+160 -28
View File
@@ -39,6 +39,14 @@ func (wm *HumanWebAuthNWriteModel) AppendEvents(events ...eventstore.EventReader
if wm.WebauthNTokenID == e.WebAuthNTokenID {
wm.WriteModel.AppendEvents(e)
}
case *user.HumanWebAuthNVerifiedEvent:
if wm.WebauthNTokenID == e.WebAuthNTokenID {
wm.WriteModel.AppendEvents(e)
}
case *user.HumanWebAuthNSignCountChangedEvent:
if wm.WebauthNTokenID == e.WebAuthNTokenID {
wm.WriteModel.AppendEvents(e)
}
case *user.HumanWebAuthNRemovedEvent:
if wm.WebauthNTokenID == e.WebAuthNTokenID {
wm.WriteModel.AppendEvents(e)
@@ -56,6 +64,8 @@ func (wm *HumanWebAuthNWriteModel) Reduce() error {
wm.appendAddedEvent(e)
case *user.HumanWebAuthNVerifiedEvent:
wm.appendVerifiedEvent(e)
case *user.HumanWebAuthNSignCountChangedEvent:
wm.SignCount = e.SignCount
case *user.HumanWebAuthNRemovedEvent:
wm.State = domain.MFAStateRemoved
case *user.UserRemovedEvent:
@@ -104,34 +114,35 @@ func NewHumanU2FTokensReadModel(userID, resourceOwner string) *HumanU2FTokensRea
}
func (wm *HumanU2FTokensReadModel) AppendEvents(events ...eventstore.EventReader) {
for _, event := range events {
switch e := event.(type) {
case *user.HumanWebAuthNAddedEvent:
wm.WriteModel.AppendEvents(e)
case *user.HumanWebAuthNVerifiedEvent:
wm.WriteModel.AppendEvents(e)
case *user.HumanWebAuthNRemovedEvent:
wm.WriteModel.AppendEvents(e)
case *user.UserRemovedEvent:
wm.WriteModel.AppendEvents(e)
}
}
wm.WriteModel.AppendEvents(events...)
}
func (wm *HumanU2FTokensReadModel) Reduce() error {
for _, event := range wm.Events {
switch e := event.(type) {
case *user.HumanWebAuthNAddedEvent:
case *user.HumanU2FAddedEvent:
token := &HumanWebAuthNWriteModel{}
token.appendAddedEvent(e)
wm.WebAuthNTokens = append(wm.WebAuthNTokens, token)
case *user.HumanWebAuthNVerifiedEvent:
token.appendAddedEvent(&e.HumanWebAuthNAddedEvent)
token.WriteModel = eventstore.WriteModel{
AggregateID: e.AggregateID(),
}
replaced := false
for i, existingTokens := range wm.WebAuthNTokens {
if existingTokens.State == domain.MFAStateNotReady {
wm.WebAuthNTokens[i] = token
replaced = true
}
}
if !replaced {
wm.WebAuthNTokens = append(wm.WebAuthNTokens, token)
}
case *user.HumanU2FVerifiedEvent:
idx, token := wm.WebAuthNTokenByID(e.WebAuthNTokenID)
if idx < 0 {
continue
}
token.appendVerifiedEvent(e)
case *user.HumanWebAuthNRemovedEvent:
token.appendVerifiedEvent(&e.HumanWebAuthNVerifiedEvent)
case *user.HumanU2FRemovedEvent:
idx, _ := wm.WebAuthNTokenByID(e.WebAuthNTokenID)
if idx < 0 {
continue
@@ -153,8 +164,7 @@ func (rm *HumanU2FTokensReadModel) Query() *eventstore.SearchQueryBuilder {
EventTypes(
user.HumanU2FTokenAddedType,
user.HumanU2FTokenVerifiedType,
user.HumanU2FTokenRemovedType,
user.UserV1MFAOTPRemovedType)
user.HumanU2FTokenRemovedType)
}
@@ -190,17 +200,29 @@ func (wm *HumanPasswordlessTokensReadModel) AppendEvents(events ...eventstore.Ev
func (wm *HumanPasswordlessTokensReadModel) Reduce() error {
for _, event := range wm.Events {
switch e := event.(type) {
case *user.HumanWebAuthNAddedEvent:
case *user.HumanPasswordlessAddedEvent:
token := &HumanWebAuthNWriteModel{}
token.appendAddedEvent(e)
wm.WebAuthNTokens = append(wm.WebAuthNTokens, token)
case *user.HumanWebAuthNVerifiedEvent:
token.appendAddedEvent(&e.HumanWebAuthNAddedEvent)
token.WriteModel = eventstore.WriteModel{
AggregateID: e.AggregateID(),
}
replaced := false
for i, existingTokens := range wm.WebAuthNTokens {
if existingTokens.State == domain.MFAStateNotReady {
wm.WebAuthNTokens[i] = token
replaced = true
}
}
if !replaced {
wm.WebAuthNTokens = append(wm.WebAuthNTokens, token)
}
case *user.HumanPasswordlessVerifiedEvent:
idx, token := wm.WebAuthNTokenByID(e.WebAuthNTokenID)
if idx < 0 {
continue
}
token.appendVerifiedEvent(e)
case *user.HumanWebAuthNRemovedEvent:
token.appendVerifiedEvent(&e.HumanWebAuthNVerifiedEvent)
case *user.HumanPasswordlessRemovedEvent:
idx, _ := wm.WebAuthNTokenByID(e.WebAuthNTokenID)
if idx < 0 {
continue
@@ -222,8 +244,7 @@ func (rm *HumanPasswordlessTokensReadModel) Query() *eventstore.SearchQueryBuild
EventTypes(
user.HumanPasswordlessTokenAddedType,
user.HumanPasswordlessTokenVerifiedType,
user.HumanPasswordlessTokenRemovedType,
user.UserV1MFAOTPRemovedType)
user.HumanPasswordlessTokenRemovedType)
}
@@ -235,3 +256,114 @@ func (wm *HumanPasswordlessTokensReadModel) WebAuthNTokenByID(id string) (idx in
}
return -1, nil
}
type HumanU2FLoginReadModel struct {
eventstore.WriteModel
AuthReqID string
Challenge string
State domain.UserState
}
func NewHumanU2FLoginReadModel(userID, authReqID, resourceOwner string) *HumanU2FLoginReadModel {
return &HumanU2FLoginReadModel{
WriteModel: eventstore.WriteModel{
AggregateID: userID,
ResourceOwner: resourceOwner,
},
AuthReqID: authReqID,
}
}
func (wm *HumanU2FLoginReadModel) AppendEvents(events ...eventstore.EventReader) {
for _, event := range events {
switch e := event.(type) {
case *user.HumanU2FBeginLoginEvent:
if e.AuthRequestInfo.ID != wm.AuthReqID {
continue
}
wm.WriteModel.AppendEvents(e)
case *user.UserRemovedEvent:
wm.WriteModel.AppendEvents(e)
}
}
}
func (wm *HumanU2FLoginReadModel) Reduce() error {
for _, event := range wm.Events {
switch e := event.(type) {
case *user.HumanU2FBeginLoginEvent:
wm.Challenge = e.Challenge
wm.State = domain.UserStateActive
case *user.UserRemovedEvent:
wm.State = domain.UserStateDeleted
}
}
return wm.WriteModel.Reduce()
}
func (rm *HumanU2FLoginReadModel) Query() *eventstore.SearchQueryBuilder {
return eventstore.NewSearchQueryBuilder(eventstore.ColumnsEvent, user.AggregateType).
AggregateIDs(rm.AggregateID).
ResourceOwner(rm.ResourceOwner).
EventTypes(
user.HumanU2FTokenBeginLoginType,
user.UserRemovedType,
)
}
type HumanPasswordlessLoginReadModel struct {
eventstore.WriteModel
AuthReqID string
Challenge string
State domain.UserState
}
func NewHumanPasswordlessLoginReadModel(userID, authReqID, resourceOwner string) *HumanPasswordlessLoginReadModel {
return &HumanPasswordlessLoginReadModel{
WriteModel: eventstore.WriteModel{
AggregateID: userID,
ResourceOwner: resourceOwner,
},
AuthReqID: authReqID,
}
}
func (wm *HumanPasswordlessLoginReadModel) AppendEvents(events ...eventstore.EventReader) {
for _, event := range events {
switch e := event.(type) {
case *user.HumanPasswordlessBeginLoginEvent:
if e.AuthRequestInfo.ID != wm.AuthReqID {
continue
}
wm.WriteModel.AppendEvents(e)
case *user.UserRemovedEvent:
wm.WriteModel.AppendEvents(e)
}
}
}
func (wm *HumanPasswordlessLoginReadModel) Reduce() error {
for _, event := range wm.Events {
switch e := event.(type) {
case *user.HumanPasswordlessBeginLoginEvent:
wm.Challenge = e.Challenge
wm.State = domain.UserStateActive
case *user.UserRemovedEvent:
wm.State = domain.UserStateDeleted
}
}
return wm.WriteModel.Reduce()
}
func (rm *HumanPasswordlessLoginReadModel) Query() *eventstore.SearchQueryBuilder {
return eventstore.NewSearchQueryBuilder(eventstore.ColumnsEvent, user.AggregateType).
AggregateIDs(rm.AggregateID).
ResourceOwner(rm.ResourceOwner).
EventTypes(
user.HumanPasswordlessTokenBeginLoginType,
user.UserRemovedType,
)
}
+1 -1
View File
@@ -51,7 +51,7 @@ func (r *CommandSide) ChangeMachine(ctx context.Context, machine *domain.Machine
changedEvent, hasChanged := existingUser.NewChangedEvent(ctx, machine.Name, machine.Description)
if !hasChanged {
return nil, caos_errs.ThrowPreconditionFailed(nil, "COMMAND-2M9fs", "Errors.User.Email.NotChanged")
return nil, caos_errs.ThrowPreconditionFailed(nil, "COMMAND-2n8vs", "Errors.User.Email.NotChanged")
}
userAgg := UserAggregateFromWriteModel(&existingUser.WriteModel)
userAgg.PushEvents(changedEvent)
+83
View File
@@ -0,0 +1,83 @@
package command
import (
"context"
"github.com/caos/logging"
"github.com/caos/zitadel/internal/errors"
"github.com/caos/zitadel/internal/telemetry/tracing"
"github.com/caos/zitadel/internal/v2/domain"
"github.com/caos/zitadel/internal/v2/repository/user"
"time"
)
const (
yearLayout = "2006-01-02"
defaultExpirationDate = "9999-01-01"
)
func (r *CommandSide) AddUserMachineKey(ctx context.Context, machineKey *domain.MachineKey, resourceOwner string) (*domain.MachineKey, error) {
err := r.checkUserExists(ctx, machineKey.AggregateID, resourceOwner)
if err != nil {
return nil, err
}
keyID, err := r.idGenerator.Next()
if err != nil {
return nil, err
}
keyWriteModel := NewMachineKeyWriteModel(machineKey.AggregateID, keyID, resourceOwner)
userAgg := UserAggregateFromWriteModel(&keyWriteModel.WriteModel)
err = r.eventstore.FilterToQueryReducer(ctx, keyWriteModel)
if err != nil {
return nil, err
}
if machineKey.ExpirationDate.IsZero() {
machineKey.ExpirationDate, err = time.Parse(yearLayout, defaultExpirationDate)
if err != nil {
logging.Log("COMMAND9-v8jMi").WithError(err).Warn("unable to set default date")
return nil, errors.ThrowInternal(err, "COMMAND-38jfus", "Errors.Internal")
}
}
if machineKey.ExpirationDate.Before(time.Now()) {
return nil, errors.ThrowInvalidArgument(nil, "COMMAND-38vns", "Errors.MachineKey.ExpireBeforeNow")
}
machineKey.GenerateNewMachineKeyPair(r.machineKeySize)
userAgg.PushEvents(user.NewMachineKeyAddedEvent(ctx, keyID, machineKey.Type, machineKey.ExpirationDate, machineKey.PublicKey))
err = r.eventstore.PushAggregate(ctx, keyWriteModel, userAgg)
if err != nil {
return nil, err
}
key := keyWriteModelToMachineKey(keyWriteModel)
key.PrivateKey = machineKey.PrivateKey
return key, nil
}
func (r *CommandSide) RemoveUserMachineKey(ctx context.Context, userID, keyID, resourceOwner string) error {
keyWriteModel, err := r.machineKeyWriteModelByID(ctx, userID, keyID, resourceOwner)
if err != nil {
return err
}
if keyWriteModel.State == domain.MachineKeyStateUnspecified || keyWriteModel.State == domain.MachineKeyStateRemoved {
return errors.ThrowNotFound(nil, "COMMAND-4m77G", "Errors.User.Machine.Key.NotFound")
}
userAgg := UserAggregateFromWriteModel(&keyWriteModel.WriteModel)
userAgg.PushEvents(user.NewMachineKeyRemovedEvent(ctx, keyID))
return r.eventstore.PushAggregate(ctx, keyWriteModel, userAgg)
}
func (r *CommandSide) machineKeyWriteModelByID(ctx context.Context, userID, keyID, resourceOwner string) (writeModel *MachineKeyWriteModel, err error) {
if userID == "" {
return nil, errors.ThrowInvalidArgument(nil, "COMMAND-4n8vs", "Errors.User.UserIDMissing")
}
ctx, span := tracing.NewSpan(ctx)
defer func() { span.EndWithError(err) }()
writeModel = NewMachineKeyWriteModel(userID, keyID, resourceOwner)
err = r.eventstore.FilterToQueryReducer(ctx, writeModel)
if err != nil {
return nil, err
}
return writeModel, nil
}
@@ -0,0 +1,74 @@
package command
import (
"github.com/caos/zitadel/internal/eventstore/v2"
"github.com/caos/zitadel/internal/v2/domain"
"github.com/caos/zitadel/internal/v2/repository/user"
"time"
)
type MachineKeyWriteModel struct {
eventstore.WriteModel
KeyID string
KeyType domain.MachineKeyType
ExpirationDate time.Time
State domain.MachineKeyState
}
func NewMachineKeyWriteModel(userID, keyID, resourceOwner string) *MachineKeyWriteModel {
return &MachineKeyWriteModel{
WriteModel: eventstore.WriteModel{
AggregateID: userID,
ResourceOwner: resourceOwner,
},
KeyID: keyID,
}
}
func (wm *MachineKeyWriteModel) AppendEvents(events ...eventstore.EventReader) {
for _, event := range events {
switch e := event.(type) {
case *user.MachineKeyAddedEvent:
if wm.KeyID != e.KeyID {
continue
}
wm.WriteModel.AppendEvents(e)
case *user.MachineKeyRemovedEvent:
if wm.KeyID != e.KeyID {
continue
}
wm.WriteModel.AppendEvents(e)
case *user.UserRemovedEvent:
wm.WriteModel.AppendEvents(e)
}
}
}
func (wm *MachineKeyWriteModel) Reduce() error {
for _, event := range wm.Events {
switch e := event.(type) {
case *user.MachineKeyAddedEvent:
wm.KeyID = e.KeyID
wm.KeyType = e.KeyType
wm.ExpirationDate = e.ExpirationDate
wm.State = domain.MachineKeyStateActive
case *user.MachineKeyRemovedEvent:
wm.State = domain.MachineKeyStateRemoved
case *user.UserRemovedEvent:
wm.State = domain.MachineKeyStateRemoved
}
}
return wm.WriteModel.Reduce()
}
func (wm *MachineKeyWriteModel) Query() *eventstore.SearchQueryBuilder {
return eventstore.NewSearchQueryBuilder(eventstore.ColumnsEvent, user.AggregateType).
AggregateIDs(wm.AggregateID).
ResourceOwner(wm.ResourceOwner).
EventTypes(
user.MachineKeyAddedEventType,
user.MachineKeyRemovedEventType,
user.UserRemovedType)
}
+2
View File
@@ -39,6 +39,8 @@ func (wm *UserWriteModel) Reduce() error {
case *user.HumanRegisteredEvent:
wm.UserName = e.UserName
wm.UserState = domain.UserStateInitial
case *user.HumanInitializedCheckSucceededEvent:
wm.UserState = domain.UserStateActive
case *user.MachineAddedEvent:
wm.UserName = e.UserName
wm.UserState = domain.UserStateActive