diff --git a/cmd/zitadel/startup.yaml b/cmd/zitadel/startup.yaml index eba0ec408c..d335c07be8 100644 --- a/cmd/zitadel/startup.yaml +++ b/cmd/zitadel/startup.yaml @@ -50,6 +50,37 @@ Auth: GatewayPort: 50021 CustomHeaders: - x-zitadel- + Repository: + SearchLimit: 100 + Eventstore: + ServiceName: 'authAPI' + Repository: + SQL: + Host: $ZITADEL_EVENTSTORE_HOST + Port: $ZITADEL_EVENTSTORE_PORT + User: 'auth' + Database: 'eventstore' + SSLmode: disable + Cache: + Type: 'fastcache' + Config: + MaxCacheSizeInByte: 10485760 #10mb + AuthRequest: + Host: $ZITADEL_EVENTSTORE_HOST + Port: $ZITADEL_EVENTSTORE_PORT + User: 'auth' + Database: 'auth' + SSLmode: disable + View: + Host: $ZITADEL_EVENTSTORE_HOST + Port: $ZITADEL_EVENTSTORE_PORT + User: 'auth' + Database: 'auth' + SSLmode: disable + Spooler: + ConcurrentTasks: 4 + BulkLimit: 100 + FailureCountUntilSkip: 5 Login: # will get port range 5003x diff --git a/cmd/zitadel/system-defaults.yaml b/cmd/zitadel/system-defaults.yaml index f3df91ff8e..f8b6e7e02a 100644 --- a/cmd/zitadel/system-defaults.yaml +++ b/cmd/zitadel/system-defaults.yaml @@ -42,6 +42,11 @@ SystemDefaults: Issuer: 'Zitadel' VerificationKey: EncryptionKeyID: $ZITADEL_OTP_VERIFICATION_KEY + VerificationLifetimes: + PasswordCheck: 240h #10d + MfaInitSkip: 720h #30d + MfaSoftwareCheck: 18h + MfaHardwareCheck: 12h DefaultPolicies: Age: Description: Standard age policy @@ -103,4 +108,4 @@ SystemDefaults: ApplicationType: 'NATIVE' AuthMethodType: 'AUTH_TYPE_NONE' Owners: - - 'zitadel-admin@caos.ch' \ No newline at end of file + - 'zitadel-admin@caos.ch' diff --git a/go.mod b/go.mod index dae0193ebc..0e22208fa8 100644 --- a/go.mod +++ b/go.mod @@ -15,7 +15,7 @@ require ( github.com/aws/aws-sdk-go v1.30.25 // indirect github.com/caos/logging v0.0.1 github.com/cockroachdb/cockroach-go v0.0.0-20200504194139-73ffeee90b62 - github.com/envoyproxy/protoc-gen-validate v0.3.0 + github.com/envoyproxy/protoc-gen-validate v0.1.0 github.com/ghodss/yaml v1.0.0 github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b github.com/golang/mock v1.4.3 diff --git a/go.sum b/go.sum index 8c051155e9..6f3e5278fb 100644 --- a/go.sum +++ b/go.sum @@ -84,8 +84,6 @@ github.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.m github.com/envoyproxy/go-control-plane v0.9.4/go.mod h1:6rpuAdCZL397s3pYoYcLgu1mIlRU8Am5FuJP05cCM98= github.com/envoyproxy/protoc-gen-validate v0.1.0 h1:EQciDnbrYxy13PgWoY8AqoxGiPrpgBZ1R8UNe3ddc+A= github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c= -github.com/envoyproxy/protoc-gen-validate v0.3.0 h1:Y2J74o+yAfcD8jpqtkLnUqRo+yshLr4eR1WPYGX0cic= -github.com/envoyproxy/protoc-gen-validate v0.3.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c= github.com/erikstmartin/go-testdb v0.0.0-20160219214506-8d10e4a1bae5 h1:Yzb9+7DPaBjB8zlTR87/ElzFsnQfuHnVUVqpZZIcV5Y= github.com/erikstmartin/go-testdb v0.0.0-20160219214506-8d10e4a1bae5/go.mod h1:a2zkGnVExMxdzMo3M0Hi/3sEU+cWnZpSni0O6/Yb/P0= github.com/ghodss/yaml v1.0.0 h1:wQHKEahhL6wmXdzwWG11gIVCkOv05bNOh+Rxn0yngAk= diff --git a/internal/auth/auth/token_verifier.go b/internal/auth/auth/token_verifier.go new file mode 100644 index 0000000000..84ba0abe38 --- /dev/null +++ b/internal/auth/auth/token_verifier.go @@ -0,0 +1,25 @@ +package auth + +import ( + "context" + "github.com/caos/zitadel/internal/api/auth" +) + +type TokenVerifier struct { +} + +func Start() (v *TokenVerifier) { + return new(TokenVerifier) +} + +func (v *TokenVerifier) VerifyAccessToken(ctx context.Context, token string) (string, string, string, error) { + return "", "", "", nil +} + +func (v *TokenVerifier) ResolveGrants(ctx context.Context, userID, orgID string) ([]*auth.Grant, error) { + return nil, nil +} + +func (v *TokenVerifier) GetProjectIDByClientID(ctx context.Context, clientID string) (string, error) { + return "", nil +} diff --git a/internal/auth/repository/auth_request.go b/internal/auth/repository/auth_request.go new file mode 100644 index 0000000000..eebdad4cbe --- /dev/null +++ b/internal/auth/repository/auth_request.go @@ -0,0 +1,15 @@ +package repository + +import ( + "context" + + "github.com/caos/zitadel/internal/auth_request/model" +) + +type AuthRequestRepository interface { + CreateAuthRequest(ctx context.Context, request *model.AuthRequest) (*model.AuthRequest, error) + AuthRequestByID(ctx context.Context, id string) (*model.AuthRequest, error) + CheckUsername(ctx context.Context, id, username string) error + VerifyPassword(ctx context.Context, id, userID, password string, info *model.BrowserInfo) error + VerifyMfaOTP(ctx context.Context, agentID, authRequestID string, code string, info *model.BrowserInfo) error +} diff --git a/internal/auth/repository/eventsourcing/eventstore/auth_request.go b/internal/auth/repository/eventsourcing/eventstore/auth_request.go new file mode 100644 index 0000000000..2cee067357 --- /dev/null +++ b/internal/auth/repository/eventsourcing/eventstore/auth_request.go @@ -0,0 +1,239 @@ +package eventstore + +import ( + "context" + "time" + + "github.com/caos/zitadel/internal/auth/repository/eventsourcing/view" + "github.com/caos/zitadel/internal/auth_request/model" + "github.com/caos/zitadel/internal/auth_request/repository/cache" + "github.com/caos/zitadel/internal/errors" + "github.com/caos/zitadel/internal/id" + user_model "github.com/caos/zitadel/internal/user/model" + user_event "github.com/caos/zitadel/internal/user/repository/eventsourcing" + view_model "github.com/caos/zitadel/internal/user/repository/view/model" +) + +type AuthRequestRepo struct { + UserEvents *user_event.UserEventstore + AuthRequests *cache.AuthRequestCache + View *view.View + + UserSessionViewProvider userSessionViewProvider + UserViewProvider userViewProvider + + IdGenerator id.Generator + + PasswordCheckLifeTime time.Duration + MfaInitSkippedLifeTime time.Duration + MfaSoftwareCheckLifeTime time.Duration + MfaHardwareCheckLifeTime time.Duration +} + +type userSessionViewProvider interface { + UserSessionByIDs(string, string) (*view_model.UserSessionView, error) + UserSessionsByAgentID(string) ([]*view_model.UserSessionView, error) +} +type userViewProvider interface { + UserByID(string) (*view_model.UserView, error) +} + +func (repo *AuthRequestRepo) Health(ctx context.Context) error { + if err := repo.UserEvents.Health(ctx); err != nil { + return err + } + return repo.AuthRequests.Health(ctx) +} + +func (repo *AuthRequestRepo) CreateAuthRequest(ctx context.Context, request *model.AuthRequest) (*model.AuthRequest, error) { + reqID, err := repo.IdGenerator.Next() + if err != nil { + return nil, err + } + request.ID = reqID + err = repo.AuthRequests.SaveAuthRequest(ctx, request) + if err != nil { + return nil, err + } + return request, nil +} + +func (repo *AuthRequestRepo) AuthRequestByID(ctx context.Context, id string) (*model.AuthRequest, error) { + request, err := repo.AuthRequests.GetAuthRequestByID(ctx, id) + if err != nil { + return nil, err + } + steps, err := repo.nextSteps(request) + if err != nil { + return nil, err + } + request.PossibleSteps = steps + return request, nil +} + +func (repo *AuthRequestRepo) CheckUsername(ctx context.Context, id, username string) error { + request, err := repo.AuthRequests.GetAuthRequestByID(ctx, id) + if err != nil { + return err + } + user, err := repo.View.UserByUsername(username) + if err != nil { + return err + } + request.UserID = user.ID + return repo.AuthRequests.SaveAuthRequest(ctx, request) +} + +func (repo *AuthRequestRepo) VerifyPassword(ctx context.Context, id, userID, password string, info *model.BrowserInfo) error { + request, err := repo.AuthRequests.GetAuthRequestByID(ctx, id) + if err != nil { + return err + } + if request.UserID == userID { + return errors.ThrowPreconditionFailed(nil, "EVENT-ds35D", "user id does not match request id ") + } + return repo.UserEvents.CheckPassword(ctx, userID, password, request.WithCurrentInfo(info)) +} + +func (repo *AuthRequestRepo) VerifyMfaOTP(ctx context.Context, authRequestID, userID string, code string, info *model.BrowserInfo) error { + request, err := repo.AuthRequests.GetAuthRequestByID(ctx, authRequestID) + if err != nil { + return err + } + if request.UserID != userID { + return errors.ThrowPreconditionFailed(nil, "EVENT-ADJ26", "user id does not match request id") + } + return repo.UserEvents.CheckMfaOTP(ctx, userID, code, request.WithCurrentInfo(info)) +} + +func (repo *AuthRequestRepo) nextSteps(request *model.AuthRequest) ([]model.NextStep, error) { + if request == nil { + return nil, errors.ThrowInvalidArgument(nil, "EVENT-ds27a", "request must not be nil") + } + steps := make([]model.NextStep, 0) + if request.UserID == "" { + if request.Prompt != model.PromptNone { + steps = append(steps, &model.LoginStep{}) + } + if request.Prompt == model.PromptSelectAccount { + users, err := repo.usersForUserSelection(request) + if err != nil { + return nil, err + } + steps = append(steps, &model.SelectUserStep{Users: users}) + } + return steps, nil + } + userSession, err := userSessionByIDs(repo.UserSessionViewProvider, request.AgentID, request.UserID) + if err != nil { + return nil, err + } + user, err := userByID(repo.UserViewProvider, request.UserID) + if err != nil { + return nil, err + } + + if !user.PasswordSet { + return append(steps, &model.InitPasswordStep{}), nil + } + + if !checkVerificationTime(userSession.PasswordVerification, repo.PasswordCheckLifeTime) { + return append(steps, &model.PasswordStep{}), nil + } + + if step, ok := repo.mfaChecked(userSession, request, user); !ok { + return append(steps, step), nil + } + + if user.PasswordChangeRequired { + steps = append(steps, &model.ChangePasswordStep{}) + } + if !user.IsEmailVerified { + steps = append(steps, &model.VerifyEMailStep{}) + } + + if user.PasswordChangeRequired || !user.IsEmailVerified { + return steps, nil + } + + //PLANNED: consent step + return append(steps, &model.RedirectToCallbackStep{}), nil +} + +func (repo *AuthRequestRepo) usersForUserSelection(request *model.AuthRequest) ([]model.UserSelection, error) { + userSessions, err := userSessionsByUserAgentID(repo.UserSessionViewProvider, request.AgentID) + if err != nil { + return nil, err + } + users := make([]model.UserSelection, len(userSessions)) + for i, session := range userSessions { + users[i] = model.UserSelection{ + UserID: session.UserID, + UserName: session.UserName, + UserSessionState: session.State, + } + } + return users, nil +} + +func (repo *AuthRequestRepo) mfaChecked(userSession *user_model.UserSessionView, request *model.AuthRequest, user *user_model.UserView) (model.NextStep, bool) { + mfaLevel := request.MfaLevel() + required := user.MfaMaxSetUp < mfaLevel + if required || !repo.mfaSkippedOrSetUp(user) { + return &model.MfaPromptStep{ + Required: required, + MfaProviders: user.MfaTypesSetupPossible(mfaLevel), + }, false + } + switch mfaLevel { + default: + fallthrough + case model.MfaLevelSoftware: + if checkVerificationTime(userSession.MfaSoftwareVerification, repo.MfaSoftwareCheckLifeTime) { + return nil, true + } + fallthrough + case model.MfaLevelHardware: + if checkVerificationTime(userSession.MfaHardwareVerification, repo.MfaHardwareCheckLifeTime) { + return nil, true + } + } + return &model.MfaVerificationStep{ + MfaProviders: user.MfaTypesAllowed(mfaLevel), + }, false +} + +func (repo *AuthRequestRepo) mfaSkippedOrSetUp(user *user_model.UserView) bool { + if user.MfaMaxSetUp >= 0 { + return true + } + return checkVerificationTime(user.MfaInitSkipped, repo.MfaInitSkippedLifeTime) +} + +func checkVerificationTime(verificationTime time.Time, lifetime time.Duration) bool { + return verificationTime.Add(lifetime).After(time.Now().UTC()) +} + +func userSessionsByUserAgentID(provider userSessionViewProvider, agentID string) ([]*user_model.UserSessionView, error) { + session, err := provider.UserSessionsByAgentID(agentID) + if err != nil { + return nil, err + } + return view_model.UserSessionsToModel(session), nil +} + +func userSessionByIDs(provider userSessionViewProvider, agentID, userID string) (*user_model.UserSessionView, error) { + session, err := provider.UserSessionByIDs(agentID, userID) + if err != nil { + return nil, err + } + return view_model.UserSessionToModel(session), nil +} + +func userByID(provider userViewProvider, userID string) (*user_model.UserView, error) { + user, err := provider.UserByID(userID) + if err != nil { + return nil, err + } + return view_model.UserToModel(user), nil +} diff --git a/internal/auth/repository/eventsourcing/eventstore/auth_request_test.go b/internal/auth/repository/eventsourcing/eventstore/auth_request_test.go new file mode 100644 index 0000000000..9500118d07 --- /dev/null +++ b/internal/auth/repository/eventsourcing/eventstore/auth_request_test.go @@ -0,0 +1,475 @@ +package eventstore + +import ( + "testing" + "time" + + "github.com/stretchr/testify/assert" + + "github.com/caos/zitadel/internal/auth/repository/eventsourcing/view" + "github.com/caos/zitadel/internal/auth_request/model" + "github.com/caos/zitadel/internal/auth_request/repository/cache" + "github.com/caos/zitadel/internal/errors" + user_model "github.com/caos/zitadel/internal/user/model" + user_event "github.com/caos/zitadel/internal/user/repository/eventsourcing" + view_model "github.com/caos/zitadel/internal/user/repository/view/model" +) + +type mockViewNoUserSession struct{} + +func (m *mockViewNoUserSession) UserSessionByIDs(string, string) (*view_model.UserSessionView, error) { + return nil, errors.ThrowNotFound(nil, "id", "user session not found") +} + +func (m *mockViewNoUserSession) UserSessionsByAgentID(string) ([]*view_model.UserSessionView, error) { + return nil, errors.ThrowInternal(nil, "id", "internal error") +} + +type mockViewUserSession struct { + PasswordVerification time.Time + MfaSoftwareVerification time.Time + Users []mockUser +} + +type mockUser struct { + UserID string + UserName string +} + +func (m *mockViewUserSession) UserSessionByIDs(string, string) (*view_model.UserSessionView, error) { + return &view_model.UserSessionView{ + PasswordVerification: m.PasswordVerification, + MfaSoftwareVerification: m.MfaSoftwareVerification, + }, nil +} + +func (m *mockViewUserSession) UserSessionsByAgentID(string) ([]*view_model.UserSessionView, error) { + sessions := make([]*view_model.UserSessionView, len(m.Users)) + for i, user := range m.Users { + sessions[i] = &view_model.UserSessionView{ + UserID: user.UserID, + UserName: user.UserName, + } + } + return sessions, nil +} + +type mockViewNoUser struct{} + +func (m *mockViewNoUser) UserByID(string) (*view_model.UserView, error) { + return nil, errors.ThrowNotFound(nil, "id", "user not found") +} + +type mockViewUser struct { + PasswordSet bool + PasswordChangeRequired bool + IsEmailVerified bool + OTPState int32 + MfaMaxSetUp int32 + MfaInitSkipped time.Time +} + +func (m *mockViewUser) UserByID(string) (*view_model.UserView, error) { + return &view_model.UserView{ + PasswordSet: m.PasswordSet, + PasswordChangeRequired: m.PasswordChangeRequired, + IsEmailVerified: m.IsEmailVerified, + OTPState: m.OTPState, + MfaMaxSetUp: m.MfaMaxSetUp, + MfaInitSkipped: m.MfaInitSkipped, + }, nil +} + +func TestAuthRequestRepo_nextSteps(t *testing.T) { + type fields struct { + UserEvents *user_event.UserEventstore + AuthRequests *cache.AuthRequestCache + View *view.View + userSessionViewProvider userSessionViewProvider + userViewProvider userViewProvider + PasswordCheckLifeTime time.Duration + MfaInitSkippedLifeTime time.Duration + MfaSoftwareCheckLifeTime time.Duration + MfaHardwareCheckLifeTime time.Duration + } + type args struct { + request *model.AuthRequest + } + tests := []struct { + name string + fields fields + args args + want []model.NextStep + wantErr func(error) bool + }{ + { + "request nil, error", + fields{}, + args{nil}, + nil, + errors.IsErrorInvalidArgument, + }, + { + "user not set, login step", + fields{}, + args{&model.AuthRequest{}}, + []model.NextStep{&model.LoginStep{}}, + nil, + }, + { + "user not set and prompt none, no step", + fields{}, + args{&model.AuthRequest{Prompt: model.PromptNone}}, + []model.NextStep{}, + nil, + }, + { + "user not set, prompt select account and internal error, internal error", + fields{ + userSessionViewProvider: &mockViewNoUserSession{}, + }, + args{&model.AuthRequest{Prompt: model.PromptSelectAccount}}, + nil, + errors.IsInternal, + }, + { + "user not set, prompt select account, login and select account steps", + fields{ + userSessionViewProvider: &mockViewUserSession{ + Users: []mockUser{ + { + "id1", + "username1", + }, + { + "id2", + "username2", + }, + }, + }, + }, + args{&model.AuthRequest{Prompt: model.PromptSelectAccount}}, + []model.NextStep{ + &model.LoginStep{}, + &model.SelectUserStep{ + Users: []model.UserSelection{ + { + UserID: "id1", + UserName: "username1", + }, + { + UserID: "id2", + UserName: "username2", + }, + }, + }}, + nil, + }, + { + "usersession not found, not found error", + fields{ + userSessionViewProvider: &mockViewNoUserSession{}, + }, + args{&model.AuthRequest{UserID: "UserID"}}, + nil, + errors.IsNotFound, + }, + { + "user not not found, not found error", + fields{ + userSessionViewProvider: &mockViewUserSession{}, + userViewProvider: &mockViewNoUser{}, + }, + args{&model.AuthRequest{UserID: "UserID"}}, + nil, + errors.IsNotFound, + }, + { + "password not set, init password step", + fields{ + userSessionViewProvider: &mockViewUserSession{}, + userViewProvider: &mockViewUser{}, + }, + args{&model.AuthRequest{UserID: "UserID"}}, + []model.NextStep{&model.InitPasswordStep{}}, + nil, + }, + { + "password not verified, password check step", + fields{ + userSessionViewProvider: &mockViewUserSession{}, + userViewProvider: &mockViewUser{ + PasswordSet: true, + }, + PasswordCheckLifeTime: 10 * 24 * time.Hour, + }, + args{&model.AuthRequest{UserID: "UserID"}}, + []model.NextStep{&model.PasswordStep{}}, + nil, + }, + { + "mfa not verified, mfa check step", + fields{ + userSessionViewProvider: &mockViewUserSession{ + PasswordVerification: time.Now().UTC().Add(-5 * time.Minute), + }, + userViewProvider: &mockViewUser{ + PasswordSet: true, + OTPState: int32(user_model.MFASTATE_READY), + MfaMaxSetUp: int32(model.MfaLevelSoftware), + }, + PasswordCheckLifeTime: 10 * 24 * time.Hour, + MfaSoftwareCheckLifeTime: 18 * time.Hour, + }, + args{&model.AuthRequest{UserID: "UserID"}}, + []model.NextStep{&model.MfaVerificationStep{ + MfaProviders: []model.MfaType{model.MfaTypeOTP}, + }}, + nil, + }, + { + "password change required and email verified, password change step", + fields{ + userSessionViewProvider: &mockViewUserSession{ + PasswordVerification: time.Now().UTC().Add(-5 * time.Minute), + MfaSoftwareVerification: time.Now().UTC().Add(-5 * time.Minute), + }, + userViewProvider: &mockViewUser{ + PasswordSet: true, + PasswordChangeRequired: true, + IsEmailVerified: true, + }, + PasswordCheckLifeTime: 10 * 24 * time.Hour, + MfaSoftwareCheckLifeTime: 18 * time.Hour, + }, + args{&model.AuthRequest{UserID: "UserID"}}, + []model.NextStep{&model.ChangePasswordStep{}}, + nil, + }, + { + "email not verified and no password change required, mail verification step", + fields{ + userSessionViewProvider: &mockViewUserSession{ + PasswordVerification: time.Now().UTC().Add(-5 * time.Minute), + MfaSoftwareVerification: time.Now().UTC().Add(-5 * time.Minute), + }, + userViewProvider: &mockViewUser{ + PasswordSet: true, + }, + PasswordCheckLifeTime: 10 * 24 * time.Hour, + MfaSoftwareCheckLifeTime: 18 * time.Hour, + }, + args{&model.AuthRequest{UserID: "UserID"}}, + []model.NextStep{&model.VerifyEMailStep{}}, + nil, + }, + { + "email not verified and password change required, mail verification step", + fields{ + userSessionViewProvider: &mockViewUserSession{ + PasswordVerification: time.Now().UTC().Add(-5 * time.Minute), + MfaSoftwareVerification: time.Now().UTC().Add(-5 * time.Minute), + }, + userViewProvider: &mockViewUser{ + PasswordSet: true, + PasswordChangeRequired: true, + }, + PasswordCheckLifeTime: 10 * 24 * time.Hour, + MfaSoftwareCheckLifeTime: 18 * time.Hour, + }, + args{&model.AuthRequest{UserID: "UserID"}}, + []model.NextStep{&model.ChangePasswordStep{}, &model.VerifyEMailStep{}}, + nil, + }, + { + "email verified and no password change required, redirect to callback step", + fields{ + userSessionViewProvider: &mockViewUserSession{ + PasswordVerification: time.Now().UTC().Add(-5 * time.Minute), + MfaSoftwareVerification: time.Now().UTC().Add(-5 * time.Minute), + }, + userViewProvider: &mockViewUser{ + PasswordSet: true, + IsEmailVerified: true, + }, + PasswordCheckLifeTime: 10 * 24 * time.Hour, + MfaSoftwareCheckLifeTime: 18 * time.Hour, + }, + args{&model.AuthRequest{UserID: "UserID"}}, + []model.NextStep{&model.RedirectToCallbackStep{}}, + nil, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + repo := &AuthRequestRepo{ + UserEvents: tt.fields.UserEvents, + AuthRequests: tt.fields.AuthRequests, + View: tt.fields.View, + UserSessionViewProvider: tt.fields.userSessionViewProvider, + UserViewProvider: tt.fields.userViewProvider, + PasswordCheckLifeTime: tt.fields.PasswordCheckLifeTime, + MfaInitSkippedLifeTime: tt.fields.MfaInitSkippedLifeTime, + MfaSoftwareCheckLifeTime: tt.fields.MfaSoftwareCheckLifeTime, + MfaHardwareCheckLifeTime: tt.fields.MfaHardwareCheckLifeTime, + } + got, err := repo.nextSteps(tt.args.request) + if (err != nil && tt.wantErr == nil) || (tt.wantErr != nil && !tt.wantErr(err)) { + t.Errorf("nextSteps() wrong error = %v", err) + return + } + assert.ElementsMatch(t, got, tt.want) + }) + } +} + +func TestAuthRequestRepo_mfaChecked(t *testing.T) { + type fields struct { + MfaInitSkippedLifeTime time.Duration + MfaSoftwareCheckLifeTime time.Duration + MfaHardwareCheckLifeTime time.Duration + } + type args struct { + userSession *user_model.UserSessionView + request *model.AuthRequest + user *user_model.UserView + } + tests := []struct { + name string + fields fields + args args + want model.NextStep + wantChecked bool + }{ + //{ + // "required, prompt and false", //TODO: enable when LevelsOfAssurance is checked + // fields{}, + // args{ + // request: &model.AuthRequest{PossibleLOAs: []model.LevelOfAssurance{}}, + // user: &user_model.UserView{ + // OTPState: user_model.MFASTATE_READY, + // }, + // }, + // false, + //}, + { + "not set up, prompt and false", + fields{ + MfaInitSkippedLifeTime: 30 * 24 * time.Hour, + }, + args{ + request: &model.AuthRequest{}, + user: &user_model.UserView{ + MfaMaxSetUp: -1, + }, + }, + &model.MfaPromptStep{ + MfaProviders: []model.MfaType{}, + }, + false, + }, + { + "checked mfa software, true", + fields{ + MfaSoftwareCheckLifeTime: 18 * time.Hour, + }, + args{ + request: &model.AuthRequest{}, + user: &user_model.UserView{ + OTPState: user_model.MFASTATE_READY, + }, + userSession: &user_model.UserSessionView{MfaSoftwareVerification: time.Now().UTC().Add(-5 * time.Hour)}, + }, + nil, + true, + }, + { + "not checked, check and false", + fields{ + MfaSoftwareCheckLifeTime: 18 * time.Hour, + }, + args{ + request: &model.AuthRequest{}, + user: &user_model.UserView{ + OTPState: user_model.MFASTATE_READY, + }, + userSession: &user_model.UserSessionView{}, + }, + + &model.MfaVerificationStep{ + MfaProviders: []model.MfaType{model.MfaTypeOTP}, + }, + false, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + repo := &AuthRequestRepo{ + MfaInitSkippedLifeTime: tt.fields.MfaInitSkippedLifeTime, + MfaSoftwareCheckLifeTime: tt.fields.MfaSoftwareCheckLifeTime, + MfaHardwareCheckLifeTime: tt.fields.MfaHardwareCheckLifeTime, + } + got, ok := repo.mfaChecked(tt.args.userSession, tt.args.request, tt.args.user) + if ok != tt.wantChecked { + t.Errorf("mfaChecked() checked = %v, want %v", ok, tt.wantChecked) + } + assert.Equal(t, tt.want, got) + }) + } +} + +func TestAuthRequestRepo_mfaSkippedOrSetUp(t *testing.T) { + type fields struct { + MfaInitSkippedLifeTime time.Duration + } + type args struct { + user *user_model.UserView + } + tests := []struct { + name string + fields fields + args args + want bool + }{ + { + "mfa set up, true", + fields{}, + args{&user_model.UserView{ + MfaMaxSetUp: model.MfaLevelSoftware, + }}, + true, + }, + { + "mfa skipped active, true", + fields{ + MfaInitSkippedLifeTime: 30 * 24 * time.Hour, + }, + args{&user_model.UserView{ + MfaMaxSetUp: -1, + MfaInitSkipped: time.Now().UTC().Add(-10 * time.Hour), + }}, + true, + }, + { + "mfa skipped inactive, false", + fields{ + MfaInitSkippedLifeTime: 30 * 24 * time.Hour, + }, + args{&user_model.UserView{ + MfaMaxSetUp: -1, + MfaInitSkipped: time.Now().UTC().Add(-40 * 24 * time.Hour), + }}, + false, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + repo := &AuthRequestRepo{ + MfaInitSkippedLifeTime: tt.fields.MfaInitSkippedLifeTime, + } + if got := repo.mfaSkippedOrSetUp(tt.args.user); got != tt.want { + t.Errorf("mfaSkippedOrSetUp() = %v, want %v", got, tt.want) + } + }) + } +} diff --git a/internal/auth/repository/eventsourcing/eventstore/token.go b/internal/auth/repository/eventsourcing/eventstore/token.go new file mode 100644 index 0000000000..bceae0a43c --- /dev/null +++ b/internal/auth/repository/eventsourcing/eventstore/token.go @@ -0,0 +1,26 @@ +package eventstore + +import ( + "context" + "time" + + "github.com/caos/zitadel/internal/auth/repository/eventsourcing/view" + token_model "github.com/caos/zitadel/internal/token/model" + token_view_model "github.com/caos/zitadel/internal/token/repository/view/model" +) + +type TokenRepo struct { + View *view.View +} + +func (repo *TokenRepo) CreateToken(ctx context.Context, agentID, applicationID, userID string, lifetime time.Duration) (*token_model.Token, error) { + token, err := repo.View.CreateToken(agentID, applicationID, userID, lifetime) + if err != nil { + return nil, err + } + return token_view_model.TokenToModel(token), nil +} + +func (repo *TokenRepo) IsTokenValid(ctx context.Context, tokenID string) (bool, error) { + return repo.View.IsTokenValid(tokenID) +} diff --git a/internal/auth/repository/eventsourcing/eventstore/user.go b/internal/auth/repository/eventsourcing/eventstore/user.go new file mode 100644 index 0000000000..776d096215 --- /dev/null +++ b/internal/auth/repository/eventsourcing/eventstore/user.go @@ -0,0 +1,129 @@ +package eventstore + +import ( + "context" + + "github.com/caos/zitadel/internal/api/auth" + "github.com/caos/zitadel/internal/auth/repository/eventsourcing/view" + "github.com/caos/zitadel/internal/errors" + es_models "github.com/caos/zitadel/internal/eventstore/models" + "github.com/caos/zitadel/internal/user/model" + user_event "github.com/caos/zitadel/internal/user/repository/eventsourcing" +) + +type UserRepo struct { + UserEvents *user_event.UserEventstore + View *view.View +} + +func (repo *UserRepo) Health(ctx context.Context) error { + return repo.UserEvents.Health(ctx) +} + +func (repo *UserRepo) Register(ctx context.Context, user *model.User, resourceOwner string) (*model.User, error) { + return repo.UserEvents.RegisterUser(ctx, user, resourceOwner) +} + +func (repo *UserRepo) MyProfile(ctx context.Context) (*model.Profile, error) { + return repo.UserEvents.ProfileByID(ctx, auth.GetCtxData(ctx).UserID) +} + +func (repo *UserRepo) ChangeMyProfile(ctx context.Context, profile *model.Profile) (*model.Profile, error) { + if err := checkIDs(ctx, profile.ObjectRoot); err != nil { + return nil, err + } + return repo.UserEvents.ChangeProfile(ctx, profile) +} + +func (repo *UserRepo) MyEmail(ctx context.Context) (*model.Email, error) { + return repo.UserEvents.EmailByID(ctx, auth.GetCtxData(ctx).UserID) +} + +func (repo *UserRepo) ChangeMyEmail(ctx context.Context, email *model.Email) (*model.Email, error) { + if err := checkIDs(ctx, email.ObjectRoot); err != nil { + return nil, err + } + return repo.UserEvents.ChangeEmail(ctx, email) +} + +func (repo *UserRepo) VerifyMyEmail(ctx context.Context, code string) error { + return repo.UserEvents.VerifyEmail(ctx, auth.GetCtxData(ctx).UserID, code) +} + +func (repo *UserRepo) ResendMyEmailVerificationMail(ctx context.Context) error { + return repo.UserEvents.CreateEmailVerificationCode(ctx, auth.GetCtxData(ctx).UserID) +} + +func (repo *UserRepo) MyPhone(ctx context.Context) (*model.Phone, error) { + return repo.UserEvents.PhoneByID(ctx, auth.GetCtxData(ctx).UserID) +} + +func (repo *UserRepo) ChangeMyPhone(ctx context.Context, phone *model.Phone) (*model.Phone, error) { + if err := checkIDs(ctx, phone.ObjectRoot); err != nil { + return nil, err + } + return repo.UserEvents.ChangePhone(ctx, phone) +} + +func (repo *UserRepo) VerifyMyPhone(ctx context.Context, code string) error { + return repo.UserEvents.VerifyPhone(ctx, auth.GetCtxData(ctx).UserID, code) +} + +func (repo *UserRepo) ResendMyPhoneVerificationCode(ctx context.Context) error { + return repo.UserEvents.CreatePhoneVerificationCode(ctx, auth.GetCtxData(ctx).UserID) +} + +func (repo *UserRepo) MyAddress(ctx context.Context) (*model.Address, error) { + return repo.UserEvents.AddressByID(ctx, auth.GetCtxData(ctx).UserID) +} + +func (repo *UserRepo) ChangeMyAddress(ctx context.Context, address *model.Address) (*model.Address, error) { + if err := checkIDs(ctx, address.ObjectRoot); err != nil { + return nil, err + } + return repo.UserEvents.ChangeAddress(ctx, address) +} + +func (repo *UserRepo) ChangeMyPassword(ctx context.Context, old, new string) error { + _, err := repo.UserEvents.ChangePassword(ctx, auth.GetCtxData(ctx).UserID, old, new) + return err +} + +func (repo *UserRepo) AddMyMfaOTP(ctx context.Context) (*model.OTP, error) { + return repo.UserEvents.AddOTP(ctx, auth.GetCtxData(ctx).UserID) +} + +func (repo *UserRepo) VerifyMyMfaOTP(ctx context.Context, code string) error { + return repo.UserEvents.CheckMfaOTPSetup(ctx, auth.GetCtxData(ctx).UserID, code) +} + +func (repo *UserRepo) RemoveMyMfaOTP(ctx context.Context) error { + return repo.UserEvents.RemoveOTP(ctx, auth.GetCtxData(ctx).UserID) +} + +func (repo *UserRepo) SkipMfaInit(ctx context.Context, userID string) error { + return repo.UserEvents.SkipMfaInit(ctx, userID) +} + +func (repo *UserRepo) RequestPasswordReset(ctx context.Context, username string) error { + user, err := repo.View.UserByUsername(username) + if err != nil { + return err + } + return repo.UserEvents.RequestSetPassword(ctx, user.ID, model.NOTIFICATIONTYPE_EMAIL) +} + +func (repo *UserRepo) SetPassword(ctx context.Context, userID, code, password string) error { + return repo.UserEvents.SetPassword(ctx, userID, code, password) +} + +func (repo *UserRepo) SignOut(ctx context.Context, agentID, userID string) error { + return repo.UserEvents.SignOut(ctx, agentID, userID) +} + +func checkIDs(ctx context.Context, obj es_models.ObjectRoot) error { + if obj.AggregateID != auth.GetCtxData(ctx).UserID { + return errors.ThrowPermissionDenied(nil, "EVENT-kFi9w", "object does not belong to user") + } + return nil +} diff --git a/internal/auth/repository/eventsourcing/handler/handler.go b/internal/auth/repository/eventsourcing/handler/handler.go new file mode 100644 index 0000000000..991bc97bd2 --- /dev/null +++ b/internal/auth/repository/eventsourcing/handler/handler.go @@ -0,0 +1,42 @@ +package handler + +import ( + "time" + + "github.com/caos/zitadel/internal/auth/repository/eventsourcing/view" + "github.com/caos/zitadel/internal/eventstore/spooler" + usr_event "github.com/caos/zitadel/internal/user/repository/eventsourcing" +) + +type Configs map[string]*Config + +type Config struct { + MinimumCycleDurationMillisecond int +} + +type handler struct { + view *view.View + bulkLimit uint64 + cycleDuration time.Duration + errorCountUntilSkip uint64 +} + +type EventstoreRepos struct { + UserEvents *usr_event.UserEventstore +} + +func Register(configs Configs, bulkLimit, errorCount uint64, view *view.View, repos EventstoreRepos) []spooler.Handler { + return []spooler.Handler{ + &User{handler: handler{view, bulkLimit, configs.cycleDuration("User"), errorCount}}, + &UserSession{handler: handler{view, bulkLimit, configs.cycleDuration("UserSession"), errorCount}, userEvents: repos.UserEvents}, + &Token{handler: handler{view, bulkLimit, configs.cycleDuration("Token"), errorCount}}, + } +} + +func (configs Configs) cycleDuration(viewModel string) time.Duration { + c, ok := configs[viewModel] + if !ok { + return 1 * time.Second + } + return time.Duration(c.MinimumCycleDurationMillisecond) * time.Millisecond +} diff --git a/internal/auth/repository/eventsourcing/handler/token.go b/internal/auth/repository/eventsourcing/handler/token.go new file mode 100644 index 0000000000..8bdedf6a76 --- /dev/null +++ b/internal/auth/repository/eventsourcing/handler/token.go @@ -0,0 +1,69 @@ +package handler + +import ( + "encoding/json" + "time" + + caos_errs "github.com/caos/zitadel/internal/errors" + es_model "github.com/caos/zitadel/internal/user/repository/eventsourcing/model" + + "github.com/caos/logging" + + "github.com/caos/zitadel/internal/eventstore/models" + "github.com/caos/zitadel/internal/eventstore/spooler" + "github.com/caos/zitadel/internal/user/repository/eventsourcing" +) + +type Token struct { + handler +} + +const ( + tokenTable = "auth.tokens" +) + +func (u *Token) MinimumCycleDuration() time.Duration { return u.cycleDuration } + +func (u *Token) ViewModel() string { + return tokenTable +} + +func (u *Token) EventQuery() (*models.SearchQuery, error) { + sequence, err := u.view.GetLatestTokenSequence() + if err != nil { + return nil, err + } + return eventsourcing.UserQuery(sequence), nil +} + +func (u *Token) Process(event *models.Event) (err error) { + switch event.Type { + case es_model.SignedOut: + id, err := agentIDFromSession(event) + if err != nil { + return err + } + err = u.view.DeleteSessionTokens(id, event.AggregateID, event.Sequence) + if err != nil { + return err + } + return u.view.ProcessedTokenSequence(event.Sequence) + default: + return u.view.ProcessedTokenSequence(event.Sequence) + } + return nil +} + +func (u *Token) OnError(event *models.Event, err error) error { + logging.LogWithFields("SPOOL-3jkl4", "id", event.AggregateID).WithError(err).Warn("something went wrong in token handler") + return spooler.HandleError(event, err, u.view.GetLatestTokenFailedEvent, u.view.ProcessedTokenFailedEvent, u.view.ProcessedTokenSequence, u.errorCountUntilSkip) +} + +func agentIDFromSession(event *models.Event) (string, error) { + session := make(map[string]interface{}) + if err := json.Unmarshal(event.Data, session); err != nil { + logging.Log("EVEN-s3bq9").WithError(err).Error("could not unmarshal event data") + return "", caos_errs.ThrowInternal(nil, "MODEL-sd325", "could not unmarshal data") + } + return session["agentID"].(string), nil +} diff --git a/internal/auth/repository/eventsourcing/handler/user.go b/internal/auth/repository/eventsourcing/handler/user.go new file mode 100644 index 0000000000..3177653e11 --- /dev/null +++ b/internal/auth/repository/eventsourcing/handler/user.go @@ -0,0 +1,77 @@ +package handler + +import ( + es_model "github.com/caos/zitadel/internal/user/repository/eventsourcing/model" + "time" + + "github.com/caos/logging" + + "github.com/caos/zitadel/internal/eventstore" + "github.com/caos/zitadel/internal/eventstore/models" + "github.com/caos/zitadel/internal/eventstore/spooler" + "github.com/caos/zitadel/internal/user/repository/eventsourcing" + view_model "github.com/caos/zitadel/internal/user/repository/view/model" +) + +type User struct { + handler + eventstore eventstore.Eventstore +} + +const ( + userTable = "auth.users" +) + +func (p *User) MinimumCycleDuration() time.Duration { return p.cycleDuration } + +func (p *User) ViewModel() string { + return userTable +} + +func (p *User) EventQuery() (*models.SearchQuery, error) { + sequence, err := p.view.GetLatestUserSequence() + if err != nil { + return nil, err + } + return eventsourcing.UserQuery(sequence), nil +} + +func (p *User) Process(event *models.Event) (err error) { + user := new(view_model.UserView) + switch event.Type { + case es_model.UserAdded, + es_model.UserRegistered: + user.AppendEvent(event) + case es_model.UserProfileChanged, + es_model.UserEmailChanged, + es_model.UserEmailVerified, + es_model.UserPhoneChanged, + es_model.UserPhoneVerified, + es_model.UserAddressChanged, + es_model.UserDeactivated, + es_model.UserReactivated, + es_model.UserLocked, + es_model.UserUnlocked, + es_model.MfaOtpAdded, + es_model.MfaOtpVerified, + es_model.MfaOtpRemoved: + user, err = p.view.UserByID(event.AggregateID) + if err != nil { + return err + } + err = user.AppendEvent(event) + case es_model.UserDeleted: + err = p.view.DeleteUser(event.AggregateID, event.Sequence) + default: + return p.view.ProcessedUserSequence(event.Sequence) + } + if err != nil { + return err + } + return p.view.PutUser(user) +} + +func (p *User) OnError(event *models.Event, err error) error { + logging.LogWithFields("SPOOL-is8wa", "id", event.AggregateID).WithError(err).Warn("something went wrong in user handler") + return spooler.HandleError(event, err, p.view.GetLatestUserFailedEvent, p.view.ProcessedUserFailedEvent, p.view.ProcessedUserSequence, p.errorCountUntilSkip) +} diff --git a/internal/auth/repository/eventsourcing/handler/user_session.go b/internal/auth/repository/eventsourcing/handler/user_session.go new file mode 100644 index 0000000000..31d8ffa112 --- /dev/null +++ b/internal/auth/repository/eventsourcing/handler/user_session.go @@ -0,0 +1,90 @@ +package handler + +import ( + "context" + "time" + + req_model "github.com/caos/zitadel/internal/auth_request/model" + "github.com/caos/zitadel/internal/errors" + es_model "github.com/caos/zitadel/internal/user/repository/eventsourcing/model" + + "github.com/caos/logging" + + "github.com/caos/zitadel/internal/eventstore/models" + "github.com/caos/zitadel/internal/eventstore/spooler" + "github.com/caos/zitadel/internal/user/repository/eventsourcing" + user_events "github.com/caos/zitadel/internal/user/repository/eventsourcing" + view_model "github.com/caos/zitadel/internal/user/repository/view/model" +) + +type UserSession struct { + handler + userEvents *user_events.UserEventstore +} + +const ( + userSessionTable = "auth.user_sessions" +) + +func (u *UserSession) MinimumCycleDuration() time.Duration { return u.cycleDuration } + +func (u *UserSession) ViewModel() string { + return userSessionTable +} + +func (u *UserSession) EventQuery() (*models.SearchQuery, error) { + sequence, err := u.view.GetLatestUserSessionSequence() + if err != nil { + return nil, err + } + return eventsourcing.UserQuery(sequence), nil +} + +func (u *UserSession) Process(event *models.Event) (err error) { + eventData, err := view_model.UserSessionFromEvent(event) + if err != nil { + return err + } + session, err := u.view.UserSessionByIDs(eventData.UserAgentID, event.AggregateID) + if err != nil { + if !errors.IsNotFound(err) { + return err + } + session = &view_model.UserSessionView{ + CreationDate: event.CreationDate, + ResourceOwner: event.ResourceOwner, + UserAgentID: eventData.UserAgentID, + UserID: event.AggregateID, + State: int32(req_model.UserSessionStateActive), + } + } + switch event.Type { + case es_model.UserPasswordCheckSucceeded, + es_model.UserPasswordCheckFailed, + es_model.UserPasswordChanged, + es_model.MfaOtpCheckSucceeded, + es_model.MfaOtpCheckFailed, + es_model.MfaOtpRemoved: + session.AppendEvent(event) + default: + return u.view.ProcessedUserSessionSequence(event.Sequence) + } + if err := u.FillUserInfo(session, event.AggregateID); err != nil { + return err + } + return u.view.PutUserSession(session) +} + +func (u *UserSession) OnError(event *models.Event, err error) error { + logging.LogWithFields("SPOOL-sdfw3s", "id", event.AggregateID).WithError(err).Warn("something went wrong in user session handler") + return spooler.HandleError(event, err, u.view.GetLatestUserSessionFailedEvent, u.view.ProcessedUserSessionFailedEvent, u.view.ProcessedUserSessionSequence, u.errorCountUntilSkip) +} + +func (u *UserSession) FillUserInfo(session *view_model.UserSessionView, id string) error { + user, err := u.userEvents.UserByID(context.Background(), id) + if err != nil { + return err + } + session.UserName = user.UserName + return nil +} diff --git a/internal/auth/repository/eventsourcing/repository.go b/internal/auth/repository/eventsourcing/repository.go new file mode 100644 index 0000000000..408104f8fb --- /dev/null +++ b/internal/auth/repository/eventsourcing/repository.go @@ -0,0 +1,93 @@ +package eventsourcing + +import ( + "context" + + "github.com/caos/zitadel/internal/auth/repository/eventsourcing/eventstore" + "github.com/caos/zitadel/internal/auth/repository/eventsourcing/handler" + "github.com/caos/zitadel/internal/auth/repository/eventsourcing/spooler" + auth_view "github.com/caos/zitadel/internal/auth/repository/eventsourcing/view" + "github.com/caos/zitadel/internal/auth_request/repository/cache" + sd "github.com/caos/zitadel/internal/config/systemdefaults" + "github.com/caos/zitadel/internal/config/types" + es_int "github.com/caos/zitadel/internal/eventstore" + es_spol "github.com/caos/zitadel/internal/eventstore/spooler" + "github.com/caos/zitadel/internal/id" + es_user "github.com/caos/zitadel/internal/user/repository/eventsourcing" +) + +type Config struct { + Eventstore es_int.Config + AuthRequest cache.Config + View types.SQL + Spooler spooler.SpoolerConfig +} + +type EsRepository struct { + spooler *es_spol.Spooler + eventstore.UserRepo + eventstore.AuthRequestRepo + eventstore.TokenRepo +} + +func Start(conf Config, systemDefaults sd.SystemDefaults) (*EsRepository, error) { + es, err := es_int.Start(conf.Eventstore) + if err != nil { + return nil, err + } + + sqlClient, err := conf.View.Start() + if err != nil { + return nil, err + } + view, err := auth_view.StartView(sqlClient) + if err != nil { + return nil, err + } + + user, err := es_user.StartUser( + es_user.UserConfig{ + Eventstore: es, + Cache: conf.Eventstore.Cache, + }, + systemDefaults, + ) + if err != nil { + return nil, err + } + authReq, err := cache.Start(conf.AuthRequest) + if err != nil { + return nil, err + } + + repos := handler.EventstoreRepos{UserEvents: user} + spool := spooler.StartSpooler(conf.Spooler, es, view, sqlClient, repos) + + return &EsRepository{ + spool, + eventstore.UserRepo{ + UserEvents: user, + View: view, + }, + eventstore.AuthRequestRepo{ + UserEvents: user, + AuthRequests: authReq, + View: view, + UserSessionViewProvider: view, + UserViewProvider: view, + IdGenerator: id.SonyFlakeGenerator, + PasswordCheckLifeTime: systemDefaults.VerificationLifetimes.PasswordCheck.Duration, + MfaInitSkippedLifeTime: systemDefaults.VerificationLifetimes.MfaInitSkip.Duration, + MfaSoftwareCheckLifeTime: systemDefaults.VerificationLifetimes.MfaSoftwareCheck.Duration, + MfaHardwareCheckLifeTime: systemDefaults.VerificationLifetimes.MfaHardwareCheck.Duration, + }, + eventstore.TokenRepo{View: view}, + }, nil +} + +func (repo *EsRepository) Health(ctx context.Context) error { + if err := repo.UserRepo.Health(ctx); err != nil { + return err + } + return repo.AuthRequestRepo.Health(ctx) +} diff --git a/internal/auth/repository/eventsourcing/spooler/lock.go b/internal/auth/repository/eventsourcing/spooler/lock.go new file mode 100644 index 0000000000..e32a480850 --- /dev/null +++ b/internal/auth/repository/eventsourcing/spooler/lock.go @@ -0,0 +1,46 @@ +package spooler + +import ( + "context" + "database/sql" + "fmt" + caos_errs "github.com/caos/zitadel/internal/errors" + "time" + + "github.com/cockroachdb/cockroach-go/crdb" +) + +const ( + lockTable = "auth.locks" + lockedUntilKey = "locked_until" + lockerIDKey = "locker_id" + objectTypeKey = "object_type" +) + +type locker struct { + dbClient *sql.DB +} + +type lock struct { + LockerID string `gorm:"column:locker_id;primary_key"` + LockedUntil time.Time `gorm:"column:locked_until"` + ViewName string `gorm:"column:object_type;primary_key"` +} + +func (l *locker) Renew(lockerID, viewModel string, waitTime time.Duration) error { + return crdb.ExecuteTx(context.Background(), l.dbClient, nil, func(tx *sql.Tx) error { + query := fmt.Sprintf("INSERT INTO %s (%s, %s, %s) VALUES ($1, $2, now()+$3) ON CONFLICT (%s) DO UPDATE SET %s = now()+$4, %s = $5 WHERE (locks.%s < now() OR locks.%s = $6) AND locks.%s = $7", + lockTable, objectTypeKey, lockerIDKey, lockedUntilKey, objectTypeKey, lockedUntilKey, lockerIDKey, lockedUntilKey, lockerIDKey, objectTypeKey) + + rs, err := tx.Exec(query, viewModel, lockerID, waitTime.Seconds(), waitTime.Seconds(), lockerID, lockerID, viewModel) + if err != nil { + tx.Rollback() + return err + } + if rows, _ := rs.RowsAffected(); rows == 0 { + tx.Rollback() + return caos_errs.ThrowAlreadyExists(nil, "SPOOL-lso0e", "view already locked") + } + return nil + }) +} diff --git a/internal/auth/repository/eventsourcing/spooler/lock_test.go b/internal/auth/repository/eventsourcing/spooler/lock_test.go new file mode 100644 index 0000000000..8718bdcb0e --- /dev/null +++ b/internal/auth/repository/eventsourcing/spooler/lock_test.go @@ -0,0 +1,127 @@ +package spooler + +import ( + "database/sql" + "testing" + "time" + + "github.com/DATA-DOG/go-sqlmock" +) + +type dbMock struct { + db *sql.DB + mock sqlmock.Sqlmock +} + +func mockDB(t *testing.T) *dbMock { + mockDB := dbMock{} + var err error + mockDB.db, mockDB.mock, err = sqlmock.New() + if err != nil { + t.Fatalf("error occured while creating stub db %v", err) + } + + mockDB.mock.MatchExpectationsInOrder(true) + + return &mockDB +} + +func (db *dbMock) expectCommit() *dbMock { + db.mock.ExpectCommit() + + return db +} + +func (db *dbMock) expectRollback() *dbMock { + db.mock.ExpectRollback() + + return db +} + +func (db *dbMock) expectBegin() *dbMock { + db.mock.ExpectBegin() + + return db +} + +func (db *dbMock) expectSavepoint() *dbMock { + db.mock.ExpectExec("SAVEPOINT").WillReturnResult(sqlmock.NewResult(1, 1)) + return db +} + +func (db *dbMock) expectReleaseSavepoint() *dbMock { + db.mock.ExpectExec("RELEASE SAVEPOINT").WillReturnResult(sqlmock.NewResult(1, 1)) + + return db +} + +func (db *dbMock) expectRenew(lockerID, view string, affectedRows int64) *dbMock { + query := db.mock. + ExpectExec(`INSERT INTO auth\.locks \(object_type, locker_id, locked_until\) VALUES \(\$1, \$2, now\(\)\+\$3\) ON CONFLICT \(object_type\) DO UPDATE SET locked_until = now\(\)\+\$4, locker_id = \$5 WHERE \(locks\.locked_until < now\(\) OR locks\.locker_id = \$6\) AND locks\.object_type = \$7`). + WithArgs(view, lockerID, sqlmock.AnyArg(), sqlmock.AnyArg(), lockerID, lockerID, view). + WillReturnResult(sqlmock.NewResult(1, 1)) + + if affectedRows == 0 { + query.WillReturnResult(sqlmock.NewResult(0, 0)) + } else { + query.WillReturnResult(sqlmock.NewResult(1, affectedRows)) + } + + return db +} + +func Test_locker_Renew(t *testing.T) { + type fields struct { + db *dbMock + } + type args struct { + lockerID string + viewModel string + waitTime time.Duration + } + tests := []struct { + name string + fields fields + args args + wantErr bool + }{ + { + name: "renew succeeded", + fields: fields{ + db: mockDB(t). + expectBegin(). + expectSavepoint(). + expectRenew("locker", "view", 1). + expectReleaseSavepoint(). + expectCommit(), + }, + args: args{lockerID: "locker", viewModel: "view", waitTime: 1 * time.Second}, + wantErr: false, + }, + { + name: "renew now rows updated", + fields: fields{ + db: mockDB(t). + expectBegin(). + expectSavepoint(). + expectRenew("locker", "view", 0). + expectRollback(), + }, + args: args{lockerID: "locker", viewModel: "view", waitTime: 1 * time.Second}, + wantErr: true, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + l := &locker{ + dbClient: tt.fields.db.db, + } + if err := l.Renew(tt.args.lockerID, tt.args.viewModel, tt.args.waitTime); (err != nil) != tt.wantErr { + t.Errorf("locker.Renew() error = %v, wantErr %v", err, tt.wantErr) + } + if err := tt.fields.db.mock.ExpectationsWereMet(); err != nil { + t.Errorf("not all database expectations met: %v", err) + } + }) + } +} diff --git a/internal/auth/repository/eventsourcing/spooler/spooler.go b/internal/auth/repository/eventsourcing/spooler/spooler.go new file mode 100644 index 0000000000..913b96237c --- /dev/null +++ b/internal/auth/repository/eventsourcing/spooler/spooler.go @@ -0,0 +1,30 @@ +package spooler + +import ( + "database/sql" + + "github.com/caos/zitadel/internal/auth/repository/eventsourcing/handler" + "github.com/caos/zitadel/internal/auth/repository/eventsourcing/view" + + "github.com/caos/zitadel/internal/eventstore" + "github.com/caos/zitadel/internal/eventstore/spooler" +) + +type SpoolerConfig struct { + BulkLimit uint64 + FailureCountUntilSkip uint64 + ConcurrentTasks int + Handlers handler.Configs +} + +func StartSpooler(c SpoolerConfig, es eventstore.Eventstore, view *view.View, sql *sql.DB, repos handler.EventstoreRepos) *spooler.Spooler { + spoolerConfig := spooler.Config{ + Eventstore: es, + Locker: &locker{dbClient: sql}, + ConcurrentTasks: c.ConcurrentTasks, + ViewHandlers: handler.Register(c.Handlers, c.BulkLimit, c.FailureCountUntilSkip, view, repos), + } + spool := spoolerConfig.New() + spool.Start() + return spool +} diff --git a/internal/auth/repository/eventsourcing/view/error_event.go b/internal/auth/repository/eventsourcing/view/error_event.go new file mode 100644 index 0000000000..6ef28c1f69 --- /dev/null +++ b/internal/auth/repository/eventsourcing/view/error_event.go @@ -0,0 +1,17 @@ +package view + +import ( + "github.com/caos/zitadel/internal/view" +) + +const ( + errTable = "auth.failed_event" +) + +func (v *View) saveFailedEvent(failedEvent *view.FailedEvent) error { + return view.SaveFailedEvent(v.Db, errTable, failedEvent) +} + +func (v *View) latestFailedEvent(viewName string, sequence uint64) (*view.FailedEvent, error) { + return view.LatestFailedEvent(v.Db, errTable, viewName, sequence) +} diff --git a/internal/auth/repository/eventsourcing/view/sequence.go b/internal/auth/repository/eventsourcing/view/sequence.go new file mode 100644 index 0000000000..3026a1e4ea --- /dev/null +++ b/internal/auth/repository/eventsourcing/view/sequence.go @@ -0,0 +1,17 @@ +package view + +import ( + "github.com/caos/zitadel/internal/view" +) + +const ( + sequencesTable = "auth.current_sequences" +) + +func (v *View) saveCurrentSequence(viewName string, sequence uint64) error { + return view.SaveCurrentSequence(v.Db, sequencesTable, viewName, sequence) +} + +func (v *View) latestSequence(viewName string) (uint64, error) { + return view.LatestSequence(v.Db, sequencesTable, viewName) +} diff --git a/internal/auth/repository/eventsourcing/view/token.go b/internal/auth/repository/eventsourcing/view/token.go new file mode 100644 index 0000000000..adae0900e1 --- /dev/null +++ b/internal/auth/repository/eventsourcing/view/token.go @@ -0,0 +1,77 @@ +package view + +import ( + "time" + + "github.com/caos/zitadel/internal/token/repository/view" + "github.com/caos/zitadel/internal/token/repository/view/model" + global_view "github.com/caos/zitadel/internal/view" +) + +const ( + tokenTable = "auth.tokens" +) + +func (v *View) TokenByID(tokenID string) (*model.Token, error) { + return view.TokenByID(v.Db, tokenTable, tokenID) +} + +func (v *View) IsTokenValid(tokenID string) (bool, error) { + return view.IsTokenValid(v.Db, tokenTable, tokenID) +} + +func (v *View) CreateToken(agentID, applicationID, userID string, lifetime time.Duration) (*model.Token, error) { + now := time.Now().UTC() + token := &model.Token{ + CreationDate: now, + UserID: userID, + ApplicationID: applicationID, + UserAgentID: agentID, + Expiration: now.Add(lifetime), + } + err := view.PutToken(v.Db, tokenTable, token) + if err != nil { + return nil, err + } + return token, nil +} + +func (v *View) PutToken(token *model.Token) error { + err := view.PutToken(v.Db, tokenTable, token) + if err != nil { + return err + } + return v.ProcessedTokenSequence(token.Sequence) +} + +func (v *View) DeleteToken(tokenID string, eventSequence uint64) error { + err := view.DeleteToken(v.Db, tokenTable, tokenID) + if err != nil { + return nil + } + return v.ProcessedTokenSequence(eventSequence) +} + +func (v *View) DeleteSessionTokens(agentID, userID string, eventSequence uint64) error { + err := view.DeleteTokens(v.Db, tokenTable, agentID, userID) + if err != nil { + return nil + } + return v.ProcessedTokenSequence(eventSequence) +} + +func (v *View) GetLatestTokenSequence() (uint64, error) { + return v.latestSequence(tokenTable) +} + +func (v *View) ProcessedTokenSequence(eventSequence uint64) error { + return v.saveCurrentSequence(tokenTable, eventSequence) +} + +func (v *View) GetLatestTokenFailedEvent(sequence uint64) (*global_view.FailedEvent, error) { + return v.latestFailedEvent(tokenTable, sequence) +} + +func (v *View) ProcessedTokenFailedEvent(failedEvent *global_view.FailedEvent) error { + return v.saveFailedEvent(failedEvent) +} diff --git a/internal/auth/repository/eventsourcing/view/user.go b/internal/auth/repository/eventsourcing/view/user.go new file mode 100644 index 0000000000..554da10064 --- /dev/null +++ b/internal/auth/repository/eventsourcing/view/user.go @@ -0,0 +1,68 @@ +package view + +import ( + usr_model "github.com/caos/zitadel/internal/user/model" + "github.com/caos/zitadel/internal/user/repository/view" + "github.com/caos/zitadel/internal/user/repository/view/model" + global_view "github.com/caos/zitadel/internal/view" +) + +const ( + userTable = "auth.users" +) + +func (v *View) UserByID(userID string) (*model.UserView, error) { + return view.UserByID(v.Db, userTable, userID) +} + +func (v *View) UserByUsername(userName string) (*model.UserView, error) { + return view.UserByUserName(v.Db, userTable, userName) +} + +func (v *View) SearchUsers(request *usr_model.UserSearchRequest) ([]*model.UserView, int, error) { + return view.SearchUsers(v.Db, userTable, request) +} + +func (v *View) GetGlobalUserByEmail(email string) (*model.UserView, error) { + return view.GetGlobalUserByEmail(v.Db, userTable, email) +} + +func (v *View) IsUserUnique(userName, email string) (bool, error) { + return view.IsUserUnique(v.Db, userTable, userName, email) +} + +func (v *View) UserMfas(userID string) ([]*usr_model.MultiFactor, error) { + return view.UserMfas(v.Db, userTable, userID) +} + +func (v *View) PutUser(user *model.UserView) error { + err := view.PutUser(v.Db, userTable, user) + if err != nil { + return err + } + return v.ProcessedUserSequence(user.Sequence) +} + +func (v *View) DeleteUser(userID string, eventSequence uint64) error { + err := view.DeleteUser(v.Db, userTable, userID) + if err != nil { + return nil + } + return v.ProcessedUserSequence(eventSequence) +} + +func (v *View) GetLatestUserSequence() (uint64, error) { + return v.latestSequence(userTable) +} + +func (v *View) ProcessedUserSequence(eventSequence uint64) error { + return v.saveCurrentSequence(userTable, eventSequence) +} + +func (v *View) GetLatestUserFailedEvent(sequence uint64) (*global_view.FailedEvent, error) { + return v.latestFailedEvent(userTable, sequence) +} + +func (v *View) ProcessedUserFailedEvent(failedEvent *global_view.FailedEvent) error { + return v.saveFailedEvent(failedEvent) +} diff --git a/internal/auth/repository/eventsourcing/view/user_session.go b/internal/auth/repository/eventsourcing/view/user_session.go new file mode 100644 index 0000000000..dbbd2d83c6 --- /dev/null +++ b/internal/auth/repository/eventsourcing/view/user_session.go @@ -0,0 +1,55 @@ +package view + +import ( + "github.com/caos/zitadel/internal/user/repository/view" + "github.com/caos/zitadel/internal/user/repository/view/model" + global_view "github.com/caos/zitadel/internal/view" +) + +const ( + userSessionTable = "auth.user_sessions" +) + +func (v *View) UserSessionByID(sessionID string) (*model.UserSessionView, error) { + return view.UserSessionByID(v.Db, userSessionTable, sessionID) +} + +func (v *View) UserSessionByIDs(agentID, userID string) (*model.UserSessionView, error) { + return view.UserSessionByIDs(v.Db, userSessionTable, agentID, userID) +} + +func (v *View) UserSessionsByAgentID(agentID string) ([]*model.UserSessionView, error) { + return view.UserSessionsByAgentID(v.Db, userSessionTable, agentID) +} + +func (v *View) PutUserSession(userSession *model.UserSessionView) error { + err := view.PutUserSession(v.Db, userSessionTable, userSession) + if err != nil { + return err + } + return v.ProcessedUserSessionSequence(userSession.Sequence) +} + +func (v *View) DeleteUserSession(sessionID string, eventSequence uint64) error { + err := view.DeleteUserSession(v.Db, userSessionTable, sessionID) + if err != nil { + return nil + } + return v.ProcessedUserSessionSequence(eventSequence) +} + +func (v *View) GetLatestUserSessionSequence() (uint64, error) { + return v.latestSequence(userSessionTable) +} + +func (v *View) ProcessedUserSessionSequence(eventSequence uint64) error { + return v.saveCurrentSequence(userSessionTable, eventSequence) +} + +func (v *View) GetLatestUserSessionFailedEvent(sequence uint64) (*global_view.FailedEvent, error) { + return v.latestFailedEvent(userSessionTable, sequence) +} + +func (v *View) ProcessedUserSessionFailedEvent(failedEvent *global_view.FailedEvent) error { + return v.saveFailedEvent(failedEvent) +} diff --git a/internal/auth/repository/eventsourcing/view/view.go b/internal/auth/repository/eventsourcing/view/view.go new file mode 100644 index 0000000000..4b8c52392d --- /dev/null +++ b/internal/auth/repository/eventsourcing/view/view.go @@ -0,0 +1,25 @@ +package view + +import ( + "database/sql" + + "github.com/jinzhu/gorm" +) + +type View struct { + Db *gorm.DB +} + +func StartView(sqlClient *sql.DB) (*View, error) { + gorm, err := gorm.Open("postgres", sqlClient) + if err != nil { + return nil, err + } + return &View{ + Db: gorm, + }, nil +} + +func (v *View) Health() (err error) { + return v.Db.DB().Ping() +} diff --git a/internal/auth/repository/repository.go b/internal/auth/repository/repository.go new file mode 100644 index 0000000000..461fce0c92 --- /dev/null +++ b/internal/auth/repository/repository.go @@ -0,0 +1,12 @@ +package repository + +import ( + "context" +) + +type Repository interface { + Health(context.Context) error + UserRepository + AuthRequestRepository + TokenRepository +} diff --git a/internal/auth/repository/token.go b/internal/auth/repository/token.go new file mode 100644 index 0000000000..1de051447d --- /dev/null +++ b/internal/auth/repository/token.go @@ -0,0 +1,13 @@ +package repository + +import ( + "context" + "time" + + "github.com/caos/zitadel/internal/token/model" +) + +type TokenRepository interface { + CreateToken(ctx context.Context, agentID, applicationID, userID string, lifetime time.Duration) (*model.Token, error) + IsTokenValid(ctx context.Context, tokenID string) (bool, error) +} diff --git a/internal/auth/repository/user.go b/internal/auth/repository/user.go new file mode 100644 index 0000000000..7fb0b62615 --- /dev/null +++ b/internal/auth/repository/user.go @@ -0,0 +1,42 @@ +package repository + +import ( + "context" + + "github.com/caos/zitadel/internal/user/model" +) + +type UserRepository interface { + Register(ctx context.Context, user *model.User, resourceOwner string) (*model.User, error) + + myUserRepo + SkipMfaInit(ctx context.Context, userID string) error + RequestPasswordReset(ctx context.Context, username string) error + SetPassword(ctx context.Context, userID, code, password string) error + + SignOut(ctx context.Context, agentID, userID string) error +} + +type myUserRepo interface { + MyProfile(ctx context.Context) (*model.Profile, error) + ChangeMyProfile(ctx context.Context, profile *model.Profile) (*model.Profile, error) + + MyEmail(ctx context.Context) (*model.Email, error) + ChangeMyEmail(ctx context.Context, email *model.Email) (*model.Email, error) + VerifyMyEmail(ctx context.Context, code string) error + ResendMyEmailVerificationMail(ctx context.Context) error + + MyPhone(ctx context.Context) (*model.Phone, error) + ChangeMyPhone(ctx context.Context, phone *model.Phone) (*model.Phone, error) + VerifyMyPhone(ctx context.Context, code string) error + ResendMyPhoneVerificationCode(ctx context.Context) error + + MyAddress(ctx context.Context) (*model.Address, error) + ChangeMyAddress(ctx context.Context, address *model.Address) (*model.Address, error) + + ChangeMyPassword(ctx context.Context, old, new string) error + + AddMyMfaOTP(ctx context.Context) (*model.OTP, error) + VerifyMyMfaOTP(ctx context.Context, code string) error + RemoveMyMfaOTP(ctx context.Context) error +} diff --git a/internal/auth_request/model/auth_request.go b/internal/auth_request/model/auth_request.go new file mode 100644 index 0000000000..ba95313b87 --- /dev/null +++ b/internal/auth_request/model/auth_request.go @@ -0,0 +1,82 @@ +package model + +import ( + "time" +) + +type AuthRequest struct { + ID string + AgentID string + CreationDate time.Time + ChangeDate time.Time + BrowserInfo *BrowserInfo + ApplicationID string + CallbackURI string + TransferState string + Prompt Prompt + PossibleLOAs []LevelOfAssurance + UiLocales []string + LoginHint string + PreselectedUserID string + MaxAuthAge uint32 + Request Request + + levelOfAssurance LevelOfAssurance + projectApplicationIDs []string + UserID string + PossibleSteps []NextStep +} + +type Prompt int32 + +const ( + PromptUnspecified Prompt = iota + PromptNone + PromptLogin + PromptConsent + PromptSelectAccount +) + +type LevelOfAssurance int + +const ( + LevelOfAssuranceNone LevelOfAssurance = iota +) + +func NewAuthRequest(id, agentID string, info *BrowserInfo, applicationID, callbackURI, transferState string, + prompt Prompt, possibleLOAs []LevelOfAssurance, uiLocales []string, loginHint, preselectedUserID string, maxAuthAge uint32, request Request) *AuthRequest { + return &AuthRequest{ + ID: id, + AgentID: agentID, + BrowserInfo: info, + ApplicationID: applicationID, + CallbackURI: callbackURI, + TransferState: transferState, + Prompt: prompt, + PossibleLOAs: possibleLOAs, + UiLocales: uiLocales, + LoginHint: loginHint, + PreselectedUserID: preselectedUserID, + MaxAuthAge: maxAuthAge, + Request: request, + } +} + +func (a *AuthRequest) IsValid() bool { + return a.ID != "" && + a.AgentID != "" && + a.BrowserInfo != nil && a.BrowserInfo.IsValid() && + a.ApplicationID != "" && + a.CallbackURI != "" && + a.Request != nil && a.Request.IsValid() +} + +func (a *AuthRequest) MfaLevel() MfaLevel { + return -1 + //PLANNED: check a.PossibleLOAs (and Prompt Login?) +} + +func (a *AuthRequest) WithCurrentInfo(info *BrowserInfo) *AuthRequest { + a.BrowserInfo = info + return a +} diff --git a/internal/auth_request/model/auth_request_test.go b/internal/auth_request/model/auth_request_test.go new file mode 100644 index 0000000000..53fc788892 --- /dev/null +++ b/internal/auth_request/model/auth_request_test.go @@ -0,0 +1,263 @@ +package model + +import ( + "net" + "reflect" + "testing" +) + +func TestAuthRequest_IsValid(t *testing.T) { + type fields struct { + ID string + AgentID string + BrowserInfo *BrowserInfo + ApplicationID string + CallbackURI string + Request Request + } + tests := []struct { + name string + fields fields + want bool + }{ + { + "missing id, false", + fields{}, + false, + }, + { + "missing agent id, false", + fields{ + ID: "id", + }, + false, + }, + { + "missing browser info, false", + fields{ + ID: "id", + AgentID: "agentID", + }, + false, + }, + { + "browser info invalid, false", + fields{ + ID: "id", + AgentID: "agentID", + BrowserInfo: &BrowserInfo{}, + }, + false, + }, + { + "missing application id, false", + fields{ + ID: "id", + AgentID: "agentID", + BrowserInfo: &BrowserInfo{ + UserAgent: "user agent", + AcceptLanguage: "accept language", + RemoteIP: net.IPv4(29, 4, 20, 19), + }, + }, + false, + }, + { + "missing callback uri, false", + fields{ + ID: "id", + AgentID: "agentID", + BrowserInfo: &BrowserInfo{ + UserAgent: "user agent", + AcceptLanguage: "accept language", + RemoteIP: net.IPv4(29, 4, 20, 19), + }, + ApplicationID: "appID", + }, + false, + }, + { + "missing request, false", + fields{ + ID: "id", + AgentID: "agentID", + BrowserInfo: &BrowserInfo{ + UserAgent: "user agent", + AcceptLanguage: "accept language", + RemoteIP: net.IPv4(29, 4, 20, 19), + }, + ApplicationID: "appID", + CallbackURI: "schema://callback", + }, + false, + }, + { + "request invalid, false", + fields{ + ID: "id", + AgentID: "agentID", + BrowserInfo: &BrowserInfo{ + UserAgent: "user agent", + AcceptLanguage: "accept language", + RemoteIP: net.IPv4(29, 4, 20, 19), + }, + ApplicationID: "appID", + CallbackURI: "schema://callback", + Request: &AuthRequestOIDC{}, + }, + false, + }, + { + "valid auth request, true", + fields{ + ID: "id", + AgentID: "agentID", + BrowserInfo: &BrowserInfo{ + UserAgent: "user agent", + AcceptLanguage: "accept language", + RemoteIP: net.IPv4(29, 4, 20, 19), + }, + ApplicationID: "appID", + CallbackURI: "schema://callback", + Request: &AuthRequestOIDC{ + Scopes: []string{"openid"}, + CodeChallenge: &OIDCCodeChallenge{ + Challenge: "challenge", + Method: CodeChallengeMethodS256, + }, + }, + }, + true, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + a := &AuthRequest{ + ID: tt.fields.ID, + AgentID: tt.fields.AgentID, + BrowserInfo: tt.fields.BrowserInfo, + ApplicationID: tt.fields.ApplicationID, + CallbackURI: tt.fields.CallbackURI, + Request: tt.fields.Request, + } + if got := a.IsValid(); got != tt.want { + t.Errorf("IsValid() = %v, want %v", got, tt.want) + } + }) + } +} + +func TestAuthRequest_MfaLevel(t *testing.T) { + type fields struct { + Prompt Prompt + PossibleLOAs []LevelOfAssurance + } + tests := []struct { + name string + fields fields + want MfaLevel + }{ + //PLANNED: Add / replace test cases when LOA is set + {"-1", + fields{}, + -1, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + a := &AuthRequest{ + Prompt: tt.fields.Prompt, + PossibleLOAs: tt.fields.PossibleLOAs, + } + if got := a.MfaLevel(); got != tt.want { + t.Errorf("MfaLevel() = %v, want %v", got, tt.want) + } + }) + } +} + +func TestAuthRequest_WithCurrentInfo(t *testing.T) { + type fields struct { + ID string + AgentID string + BrowserInfo *BrowserInfo + } + type args struct { + info *BrowserInfo + } + tests := []struct { + name string + fields fields + args args + want *AuthRequest + }{ + { + "unchanged", + fields{ + ID: "id", + AgentID: "agentID", + BrowserInfo: &BrowserInfo{ + UserAgent: "ua", + AcceptLanguage: "de", + RemoteIP: net.IPv4(29, 4, 20, 19), + }, + }, + args{ + &BrowserInfo{ + UserAgent: "ua", + AcceptLanguage: "de", + RemoteIP: net.IPv4(29, 4, 20, 19), + }, + }, + &AuthRequest{ + ID: "id", + AgentID: "agentID", + BrowserInfo: &BrowserInfo{ + UserAgent: "ua", + AcceptLanguage: "de", + RemoteIP: net.IPv4(29, 4, 20, 19), + }, + }, + }, + { + "changed", + fields{ + ID: "id", + AgentID: "agentID", + BrowserInfo: &BrowserInfo{ + UserAgent: "ua", + AcceptLanguage: "de", + RemoteIP: net.IPv4(29, 4, 20, 19), + }, + }, + args{ + &BrowserInfo{ + UserAgent: "ua", + AcceptLanguage: "de", + RemoteIP: net.IPv4(16, 12, 20, 19), + }, + }, + &AuthRequest{ + ID: "id", + AgentID: "agentID", + BrowserInfo: &BrowserInfo{ + UserAgent: "ua", + AcceptLanguage: "de", + RemoteIP: net.IPv4(16, 12, 20, 19), + }, + }, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + a := &AuthRequest{ + ID: tt.fields.ID, + AgentID: tt.fields.AgentID, + BrowserInfo: tt.fields.BrowserInfo, + } + if got := a.WithCurrentInfo(tt.args.info); !reflect.DeepEqual(got, tt.want) { + t.Errorf("WithCurrentInfo() = %v, want %v", got, tt.want) + } + }) + } +} diff --git a/internal/auth_request/model/browser_info.go b/internal/auth_request/model/browser_info.go new file mode 100644 index 0000000000..2812b88583 --- /dev/null +++ b/internal/auth_request/model/browser_info.go @@ -0,0 +1,15 @@ +package model + +import "net" + +type BrowserInfo struct { + UserAgent string + AcceptLanguage string + RemoteIP net.IP +} + +func (i *BrowserInfo) IsValid() bool { + return i.UserAgent != "" && + i.AcceptLanguage != "" && + i.RemoteIP != nil && !i.RemoteIP.IsUnspecified() +} diff --git a/internal/auth_request/model/code_challenge.go b/internal/auth_request/model/code_challenge.go new file mode 100644 index 0000000000..eada208686 --- /dev/null +++ b/internal/auth_request/model/code_challenge.go @@ -0,0 +1,17 @@ +package model + +type OIDCCodeChallenge struct { + Challenge string + Method OIDCCodeChallengeMethod +} + +func (c *OIDCCodeChallenge) IsValid() bool { + return c.Challenge != "" +} + +type OIDCCodeChallengeMethod int32 + +const ( + CodeChallengeMethodPlain OIDCCodeChallengeMethod = iota + CodeChallengeMethodS256 +) diff --git a/internal/auth_request/model/next_step.go b/internal/auth_request/model/next_step.go new file mode 100644 index 0000000000..68a9beb7ab --- /dev/null +++ b/internal/auth_request/model/next_step.go @@ -0,0 +1,117 @@ +package model + +type NextStep interface { + Type() NextStepType +} + +type NextStepType int32 + +const ( + NextStepUnspecified NextStepType = iota + NextStepLogin + NextStepUserSelection + NextStepPassword + NextStepChangePassword + NextStepInitPassword + NextStepVerifyEmail + NextStepMfaPrompt + NextStepMfaVerify + NextStepRedirectToCallback +) + +type UserSessionState int32 + +const ( + UserSessionStateActive UserSessionState = iota + UserSessionStateTerminated +) + +type LoginStep struct { + NotFound bool +} + +func (s *LoginStep) Type() NextStepType { + return NextStepLogin +} + +type SelectUserStep struct { + Users []UserSelection +} + +func (s *SelectUserStep) Type() NextStepType { + return NextStepUserSelection +} + +type UserSelection struct { + UserID string + UserName string + UserSessionState UserSessionState +} + +type PasswordStep struct { + FailureCount uint16 +} + +func (s *PasswordStep) Type() NextStepType { + return NextStepPassword +} + +type ChangePasswordStep struct { +} + +func (s *ChangePasswordStep) Type() NextStepType { + return NextStepChangePassword +} + +type InitPasswordStep struct { +} + +func (s *InitPasswordStep) Type() NextStepType { + return NextStepInitPassword +} + +type VerifyEMailStep struct { +} + +func (s *VerifyEMailStep) Type() NextStepType { + return NextStepVerifyEmail +} + +type MfaPromptStep struct { + Required bool + MfaProviders []MfaType +} + +func (s *MfaPromptStep) Type() NextStepType { + return NextStepMfaPrompt +} + +type MfaVerificationStep struct { + FailureCount uint16 + MfaProviders []MfaType +} + +func (s *MfaVerificationStep) Type() NextStepType { + return NextStepMfaVerify +} + +type RedirectToCallbackStep struct { +} + +func (s *RedirectToCallbackStep) Type() NextStepType { + return NextStepRedirectToCallback +} + +type MfaType int + +const ( + MfaTypeOTP MfaType = iota +) + +type MfaLevel int + +const ( + MfaLevelSoftware MfaLevel = iota + MfaLevelHardware + MfaLevelHardwareCertified +) diff --git a/internal/auth_request/model/request.go b/internal/auth_request/model/request.go new file mode 100644 index 0000000000..c90cd52682 --- /dev/null +++ b/internal/auth_request/model/request.go @@ -0,0 +1,48 @@ +package model + +type Request interface { + Type() AuthRequestType + IsValid() bool +} + +type AuthRequestType int32 + +const ( + AuthRequestTypeOIDC AuthRequestType = iota + AuthRequestTypeSAML +) + +type AuthRequestOIDC struct { + Scopes []string + ResponseType OIDCResponseType + Nonce string + CodeChallenge *OIDCCodeChallenge +} + +func (a *AuthRequestOIDC) Type() AuthRequestType { + return AuthRequestTypeOIDC +} + +func (a *AuthRequestOIDC) IsValid() bool { + return len(a.Scopes) > 0 && + a.CodeChallenge == nil || a.CodeChallenge != nil && a.CodeChallenge.IsValid() +} + +type AuthRequestSAML struct { +} + +func (a *AuthRequestSAML) Type() AuthRequestType { + return AuthRequestTypeSAML +} + +func (a *AuthRequestSAML) IsValid() bool { + return true +} + +type OIDCResponseType int32 + +const ( + OIDCResponseTypeCode OIDCResponseType = iota + OIDCResponseTypeIdToken + OIDCResponseTypeToken +) diff --git a/internal/auth_request/repository/cache/cache.go b/internal/auth_request/repository/cache/cache.go new file mode 100644 index 0000000000..7f510bfccf --- /dev/null +++ b/internal/auth_request/repository/cache/cache.go @@ -0,0 +1,67 @@ +package cache + +import ( + "context" + "database/sql" + "encoding/json" + "errors" + + "github.com/caos/zitadel/internal/auth_request/model" + "github.com/caos/zitadel/internal/config/types" + caos_errs "github.com/caos/zitadel/internal/errors" +) + +type Config struct { + Connection types.SQL +} + +type AuthRequestCache struct { + client *sql.DB +} + +func Start(conf Config) (*AuthRequestCache, error) { + client, err := sql.Open("postgres", conf.Connection.ConnectionString()) + if err != nil { + return nil, caos_errs.ThrowPreconditionFailed(err, "SQL-9qBtr", "unable to open database connection") + } + return &AuthRequestCache{ + client: client, + }, nil +} + +func (c *AuthRequestCache) Health(ctx context.Context) error { + return c.client.PingContext(ctx) +} + +func (c *AuthRequestCache) GetAuthRequestByID(_ context.Context, id string) (*model.AuthRequest, error) { + var b []byte + err := c.client.QueryRow("SELECT request FROM auth.authrequests WHERE id = ?", id).Scan(&b) + if err != nil { + if errors.Is(err, sql.ErrNoRows) { + return nil, caos_errs.ThrowNotFound(err, "CACHE-d24aD", "auth request not found") + } + return nil, caos_errs.ThrowInternal(err, "CACHE-as3kj", "unable to get auth request from database") + } + request := new(model.AuthRequest) + err = json.Unmarshal(b, &request) + if err != nil { + return nil, caos_errs.ThrowInternal(err, "CACHE-2wshg", "unable to unmarshal auth request") + } + return request, nil +} + +func (c *AuthRequestCache) SaveAuthRequest(_ context.Context, request *model.AuthRequest) error { + b, err := json.Marshal(request) + if err != nil { + return caos_errs.ThrowInternal(err, "CACHE-32FH9", "unable to marshal auth request") + } + stmt, err := c.client.Prepare("INSERT INTO auth.authrequests (id, request) VALUES($1, $2)") + if err != nil { + return caos_errs.ThrowInternal(err, "CACHE-dswfF", "sql prepare failed") + } + _, err = stmt.Exec(request.ID, b) + if err != nil { + return caos_errs.ThrowInternal(err, "CACHE-sw4af", "unable to save auth request") + } + return nil +} diff --git a/internal/auth_request/repository/gen_mock.go b/internal/auth_request/repository/gen_mock.go new file mode 100644 index 0000000000..1a7097ae95 --- /dev/null +++ b/internal/auth_request/repository/gen_mock.go @@ -0,0 +1,3 @@ +package repository + +//go:generate mockgen -package mock -destination ./mock/repository.mock.go github.com/caos/zitadel/internal/auth_request/repository Repository diff --git a/internal/auth_request/repository/mock/repository.go b/internal/auth_request/repository/mock/repository.go new file mode 100644 index 0000000000..0eef6e337e --- /dev/null +++ b/internal/auth_request/repository/mock/repository.go @@ -0,0 +1,12 @@ +package mock + +import ( + "github.com/golang/mock/gomock" + + "github.com/caos/zitadel/internal/auth_request/repository" +) + +func NewMockAuthRequestRepository(ctrl *gomock.Controller) repository.Repository { + repo := NewMockRepository(ctrl) + return repo +} diff --git a/internal/auth_request/repository/mock/repository.mock.go b/internal/auth_request/repository/mock/repository.mock.go new file mode 100644 index 0000000000..8ffbf9d4a3 --- /dev/null +++ b/internal/auth_request/repository/mock/repository.mock.go @@ -0,0 +1,79 @@ +// Code generated by MockGen. DO NOT EDIT. +// Source: github.com/caos/zitadel/internal/auth_request/repository (interfaces: Repository) + +// Package mock is a generated GoMock package. +package mock + +import ( + context "context" + model "github.com/caos/zitadel/internal/auth_request/model" + gomock "github.com/golang/mock/gomock" + reflect "reflect" +) + +// MockRepository is a mock of Repository interface +type MockRepository struct { + ctrl *gomock.Controller + recorder *MockRepositoryMockRecorder +} + +// MockRepositoryMockRecorder is the mock recorder for MockRepository +type MockRepositoryMockRecorder struct { + mock *MockRepository +} + +// NewMockRepository creates a new mock instance +func NewMockRepository(ctrl *gomock.Controller) *MockRepository { + mock := &MockRepository{ctrl: ctrl} + mock.recorder = &MockRepositoryMockRecorder{mock} + return mock +} + +// EXPECT returns an object that allows the caller to indicate expected use +func (m *MockRepository) EXPECT() *MockRepositoryMockRecorder { + return m.recorder +} + +// GetAuthRequestByID mocks base method +func (m *MockRepository) GetAuthRequestByID(arg0 context.Context, arg1 string) (*model.AuthRequest, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "GetAuthRequestByID", arg0, arg1) + ret0, _ := ret[0].(*model.AuthRequest) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// GetAuthRequestByID indicates an expected call of GetAuthRequestByID +func (mr *MockRepositoryMockRecorder) GetAuthRequestByID(arg0, arg1 interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetAuthRequestByID", reflect.TypeOf((*MockRepository)(nil).GetAuthRequestByID), arg0, arg1) +} + +// Health mocks base method +func (m *MockRepository) Health(arg0 context.Context) error { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "Health", arg0) + ret0, _ := ret[0].(error) + return ret0 +} + +// Health indicates an expected call of Health +func (mr *MockRepositoryMockRecorder) Health(arg0 interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Health", reflect.TypeOf((*MockRepository)(nil).Health), arg0) +} + +// SaveAuthRequest mocks base method +func (m *MockRepository) SaveAuthRequest(arg0 context.Context, arg1 string) (*model.AuthRequest, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "SaveAuthRequest", arg0, arg1) + ret0, _ := ret[0].(*model.AuthRequest) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// SaveAuthRequest indicates an expected call of SaveAuthRequest +func (mr *MockRepositoryMockRecorder) SaveAuthRequest(arg0, arg1 interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "SaveAuthRequest", reflect.TypeOf((*MockRepository)(nil).SaveAuthRequest), arg0, arg1) +} diff --git a/internal/auth_request/repository/repository.go b/internal/auth_request/repository/repository.go new file mode 100644 index 0000000000..34e4963f13 --- /dev/null +++ b/internal/auth_request/repository/repository.go @@ -0,0 +1,14 @@ +package repository + +import ( + "context" + + "github.com/caos/zitadel/internal/auth_request/model" +) + +type Repository interface { + Health(ctx context.Context) error + + GetAuthRequestByID(ctx context.Context, id string) (*model.AuthRequest, error) + SaveAuthRequest(ctx context.Context, id string) (*model.AuthRequest, error) +} diff --git a/internal/config/systemdefaults/system_defaults.go b/internal/config/systemdefaults/system_defaults.go index 2696cf071b..276b7bc784 100644 --- a/internal/config/systemdefaults/system_defaults.go +++ b/internal/config/systemdefaults/system_defaults.go @@ -7,12 +7,13 @@ import ( ) type SystemDefaults struct { - SecretGenerators SecretGenerators - UserVerificationKey *crypto.KeyConfig - Multifactors MultifactorConfig - DefaultPolicies DefaultPolicies - IamID string - SetUp types.IAMSetUp + SecretGenerators SecretGenerators + UserVerificationKey *crypto.KeyConfig + Multifactors MultifactorConfig + VerificationLifetimes VerificationLifetimes + DefaultPolicies DefaultPolicies + IamID string + SetUp types.IAMSetUp } type SecretGenerators struct { @@ -33,6 +34,13 @@ type OTPConfig struct { VerificationKey *crypto.KeyConfig } +type VerificationLifetimes struct { + PasswordCheck types.Duration + MfaInitSkip types.Duration + MfaSoftwareCheck types.Duration + MfaHardwareCheck types.Duration +} + type DefaultPolicies struct { Age pol.PasswordAgePolicyDefault Complexity pol.PasswordComplexityPolicyDefault diff --git a/internal/crypto/code_mocker.go b/internal/crypto/code_mocker.go index ab24ab80bf..dd441259d9 100644 --- a/internal/crypto/code_mocker.go +++ b/internal/crypto/code_mocker.go @@ -27,8 +27,8 @@ func CreateMockEncryptionAlg(ctrl *gomock.Controller) EncryptionAlgorithm { return mCrypto } -func createMockHashAlg(t *testing.T) HashAlgorithm { - mCrypto := NewMockHashAlgorithm(gomock.NewController(t)) +func CreateMockHashAlg(ctrl *gomock.Controller) HashAlgorithm { + mCrypto := NewMockHashAlgorithm(ctrl) mCrypto.EXPECT().Algorithm().AnyTimes().Return("hash") mCrypto.EXPECT().Hash(gomock.Any()).DoAndReturn( func(code []byte) ([]byte, error) { diff --git a/internal/crypto/code_test.go b/internal/crypto/code_test.go index 5bb24b7e9a..47c173d77a 100644 --- a/internal/crypto/code_test.go +++ b/internal/crypto/code_test.go @@ -113,7 +113,7 @@ func TestVerifyCode(t *testing.T) { Crypted: []byte("code"), }, verificationCode: "code", - g: createMockGenerator(t, createMockHashAlg(t)), + g: createMockGenerator(t, CreateMockHashAlg(gomock.NewController(t))), }, false, }, @@ -240,7 +240,7 @@ func Test_verifyHashedCode(t *testing.T) { args{ cryptoCode: nil, verificationCode: "", - alg: createMockHashAlg(t), + alg: CreateMockHashAlg(gomock.NewController(t)), }, true, }, @@ -252,7 +252,7 @@ func Test_verifyHashedCode(t *testing.T) { Crypted: nil, }, verificationCode: "", - alg: createMockHashAlg(t), + alg: CreateMockHashAlg(gomock.NewController(t)), }, true, }, @@ -265,7 +265,7 @@ func Test_verifyHashedCode(t *testing.T) { Crypted: nil, }, verificationCode: "", - alg: createMockHashAlg(t), + alg: CreateMockHashAlg(gomock.NewController(t)), }, true, }, @@ -278,7 +278,7 @@ func Test_verifyHashedCode(t *testing.T) { Crypted: []byte("code"), }, verificationCode: "wrong", - alg: createMockHashAlg(t), + alg: CreateMockHashAlg(gomock.NewController(t)), }, true, }, @@ -291,7 +291,7 @@ func Test_verifyHashedCode(t *testing.T) { Crypted: []byte("code"), }, verificationCode: "code", - alg: createMockHashAlg(t), + alg: CreateMockHashAlg(gomock.NewController(t)), }, false, }, diff --git a/internal/crypto/crypto.go b/internal/crypto/crypto.go index d7f3269321..4dec2ecd09 100644 --- a/internal/crypto/crypto.go +++ b/internal/crypto/crypto.go @@ -100,7 +100,7 @@ func Hash(value []byte, alg HashAlgorithm) (*CryptoValue, error) { func CompareHash(value *CryptoValue, comparer []byte, alg HashAlgorithm) error { if value.Algorithm != alg.Algorithm() { - return errors.ThrowInvalidArgument(nil, "CRYPT-HF32f", "value was hash with a different algorithm") + return errors.ThrowInvalidArgument(nil, "CRYPT-HF32f", "value was hashed with a different algorithm") } return alg.CompareHash(value.Crypted, comparer) } diff --git a/internal/eventstore/sdk/sdk.go b/internal/eventstore/sdk/sdk.go index c89faf2781..cf231d44c2 100644 --- a/internal/eventstore/sdk/sdk.go +++ b/internal/eventstore/sdk/sdk.go @@ -10,7 +10,7 @@ import ( type filterFunc func(context.Context, *es_models.SearchQuery) ([]*es_models.Event, error) type appendFunc func(...*es_models.Event) error -type aggregateFunc func(context.Context) (*es_models.Aggregate, error) +type AggregateFunc func(context.Context) (*es_models.Aggregate, error) type pushFunc func(context.Context, ...*es_models.Aggregate) error func Filter(ctx context.Context, filter filterFunc, appender appendFunc, query *es_models.SearchQuery) error { @@ -32,7 +32,7 @@ func Filter(ctx context.Context, filter filterFunc, appender appendFunc, query * // Push creates the aggregates from aggregater // and pushes the aggregates to the given pushFunc // the given events are appended by the appender -func Push(ctx context.Context, push pushFunc, appender appendFunc, aggregaters ...aggregateFunc) (err error) { +func Push(ctx context.Context, push pushFunc, appender appendFunc, aggregaters ...AggregateFunc) (err error) { if len(aggregaters) < 1 { return errors.ThrowPreconditionFailed(nil, "SDK-q9wjp", "no aggregaters passed") } @@ -73,7 +73,7 @@ func appendAggregates(appender appendFunc, aggregates []*models.Aggregate) error return nil } -func makeAggregates(ctx context.Context, aggregaters []aggregateFunc) (aggregates []*models.Aggregate, err error) { +func makeAggregates(ctx context.Context, aggregaters []AggregateFunc) (aggregates []*models.Aggregate, err error) { aggregates = make([]*models.Aggregate, len(aggregaters)) for i, aggregater := range aggregaters { aggregates[i], err = aggregater(ctx) diff --git a/internal/eventstore/sdk/sdk_test.go b/internal/eventstore/sdk/sdk_test.go index 63021321b4..25c7a577dc 100644 --- a/internal/eventstore/sdk/sdk_test.go +++ b/internal/eventstore/sdk/sdk_test.go @@ -80,7 +80,7 @@ func TestPush(t *testing.T) { type args struct { push pushFunc appender appendFunc - aggregaters []aggregateFunc + aggregaters []AggregateFunc } tests := []struct { name string @@ -101,7 +101,7 @@ func TestPush(t *testing.T) { args: args{ push: nil, appender: nil, - aggregaters: []aggregateFunc{ + aggregaters: []AggregateFunc{ func(context.Context) (*es_models.Aggregate, error) { return nil, errors.ThrowInternal(nil, "SDK-Ec5x2", "test err") }, @@ -116,7 +116,7 @@ func TestPush(t *testing.T) { return errors.ThrowInternal(nil, "SDK-0g4gW", "test error") }, appender: nil, - aggregaters: []aggregateFunc{ + aggregaters: []AggregateFunc{ func(context.Context) (*es_models.Aggregate, error) { return &es_models.Aggregate{}, nil }, @@ -133,7 +133,7 @@ func TestPush(t *testing.T) { appender: func(...*es_models.Event) error { return errors.ThrowInvalidArgument(nil, "SDK-BDhcT", "test err") }, - aggregaters: []aggregateFunc{ + aggregaters: []AggregateFunc{ func(context.Context) (*es_models.Aggregate, error) { return &es_models.Aggregate{Events: []*es_models.Event{&es_models.Event{}}}, nil }, @@ -150,7 +150,7 @@ func TestPush(t *testing.T) { appender: func(...*es_models.Event) error { return nil }, - aggregaters: []aggregateFunc{ + aggregaters: []AggregateFunc{ func(context.Context) (*es_models.Aggregate, error) { return &es_models.Aggregate{Events: []*es_models.Event{&es_models.Event{}}}, nil }, @@ -167,7 +167,7 @@ func TestPush(t *testing.T) { appender: func(...*es_models.Event) error { return nil }, - aggregaters: []aggregateFunc{ + aggregaters: []AggregateFunc{ func(context.Context) (*es_models.Aggregate, error) { return &es_models.Aggregate{Events: []*es_models.Event{&es_models.Event{}}}, nil }, diff --git a/internal/id/gen_mock.go b/internal/id/gen_mock.go new file mode 100644 index 0000000000..db0189545e --- /dev/null +++ b/internal/id/gen_mock.go @@ -0,0 +1,3 @@ +package id + +//go:generate mockgen -package mock -destination ./mock/generator.mock.go github.com/caos/zitadel/internal/id Generator diff --git a/internal/id/id_generator.go b/internal/id/id_generator.go new file mode 100644 index 0000000000..0c2e68f1f1 --- /dev/null +++ b/internal/id/id_generator.go @@ -0,0 +1,5 @@ +package id + +type Generator interface { + Next() (string, error) +} diff --git a/internal/id/mock/generator.mock.go b/internal/id/mock/generator.mock.go new file mode 100644 index 0000000000..559379eb22 --- /dev/null +++ b/internal/id/mock/generator.mock.go @@ -0,0 +1,48 @@ +// Code generated by MockGen. DO NOT EDIT. +// Source: github.com/caos/zitadel/internal/id (interfaces: Generator) + +// Package mock is a generated GoMock package. +package mock + +import ( + gomock "github.com/golang/mock/gomock" + reflect "reflect" +) + +// MockGenerator is a mock of Generator interface +type MockGenerator struct { + ctrl *gomock.Controller + recorder *MockGeneratorMockRecorder +} + +// MockGeneratorMockRecorder is the mock recorder for MockGenerator +type MockGeneratorMockRecorder struct { + mock *MockGenerator +} + +// NewMockGenerator creates a new mock instance +func NewMockGenerator(ctrl *gomock.Controller) *MockGenerator { + mock := &MockGenerator{ctrl: ctrl} + mock.recorder = &MockGeneratorMockRecorder{mock} + return mock +} + +// EXPECT returns an object that allows the caller to indicate expected use +func (m *MockGenerator) EXPECT() *MockGeneratorMockRecorder { + return m.recorder +} + +// Next mocks base method +func (m *MockGenerator) Next() (string, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "Next") + ret0, _ := ret[0].(string) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// Next indicates an expected call of Next +func (mr *MockGeneratorMockRecorder) Next() *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Next", reflect.TypeOf((*MockGenerator)(nil).Next)) +} diff --git a/internal/id/sonyflake.go b/internal/id/sonyflake.go new file mode 100644 index 0000000000..abb46f689a --- /dev/null +++ b/internal/id/sonyflake.go @@ -0,0 +1,28 @@ +package id + +import ( + "strconv" + "time" + + "github.com/sony/sonyflake" +) + +type sonyflakeGenerator struct { + *sonyflake.Sonyflake +} + +func (s *sonyflakeGenerator) Next() (string, error) { + id, err := s.NextID() + if err != nil { + return "", err + } + return strconv.FormatUint(id, 10), nil +} + +var ( + SonyFlakeGenerator = Generator(&sonyflakeGenerator{ + sonyflake.NewSonyflake(sonyflake.Settings{ + StartTime: time.Date(2019, 4, 29, 0, 0, 0, 0, time.UTC), + }), + }) +) diff --git a/internal/management/repository/eventsourcing/user.go b/internal/management/repository/eventsourcing/user.go new file mode 100644 index 0000000000..ed845ffbf4 --- /dev/null +++ b/internal/management/repository/eventsourcing/user.go @@ -0,0 +1,87 @@ +package eventsourcing + +import ( + "context" + usr_model "github.com/caos/zitadel/internal/user/model" + usr_event "github.com/caos/zitadel/internal/user/repository/eventsourcing" +) + +type UserRepo struct { + UserEvents *usr_event.UserEventstore +} + +func (repo *UserRepo) UserByID(ctx context.Context, id string) (project *usr_model.User, err error) { + return repo.UserEvents.UserByID(ctx, id) +} + +func (repo *UserRepo) CreateUser(ctx context.Context, user *usr_model.User) (*usr_model.User, error) { + return repo.UserEvents.CreateUser(ctx, user) +} + +func (repo *UserRepo) RegisterUser(ctx context.Context, user *usr_model.User, resourceOwner string) (*usr_model.User, error) { + return repo.UserEvents.RegisterUser(ctx, user, resourceOwner) +} + +func (repo *UserRepo) DeactivateUser(ctx context.Context, id string) (*usr_model.User, error) { + return repo.UserEvents.DeactivateUser(ctx, id) +} + +func (repo *UserRepo) ReactivateUser(ctx context.Context, id string) (*usr_model.User, error) { + return repo.UserEvents.ReactivateUser(ctx, id) +} + +func (repo *UserRepo) LockUser(ctx context.Context, id string) (*usr_model.User, error) { + return repo.UserEvents.LockUser(ctx, id) +} + +func (repo *UserRepo) UnlockUser(ctx context.Context, id string) (*usr_model.User, error) { + return repo.UserEvents.UnlockUser(ctx, id) +} + +func (repo *UserRepo) SetOneTimePassword(ctx context.Context, password *usr_model.Password) (*usr_model.Password, error) { + return repo.UserEvents.SetOneTimePassword(ctx, password) +} + +func (repo *UserRepo) RequestSetPassword(ctx context.Context, id string, notifyType usr_model.NotificationType) error { + return repo.UserEvents.RequestSetPassword(ctx, id, notifyType) +} + +func (repo *UserRepo) ProfileByID(ctx context.Context, userID string) (*usr_model.Profile, error) { + return repo.UserEvents.ProfileByID(ctx, userID) +} + +func (repo *UserRepo) ChangeProfile(ctx context.Context, profile *usr_model.Profile) (*usr_model.Profile, error) { + return repo.UserEvents.ChangeProfile(ctx, profile) +} + +func (repo *UserRepo) EmailByID(ctx context.Context, userID string) (*usr_model.Email, error) { + return repo.UserEvents.EmailByID(ctx, userID) +} + +func (repo *UserRepo) ChangeEmail(ctx context.Context, email *usr_model.Email) (*usr_model.Email, error) { + return repo.UserEvents.ChangeEmail(ctx, email) +} + +func (repo *UserRepo) CreateEmailVerificationCode(ctx context.Context, userID string) error { + return repo.UserEvents.CreateEmailVerificationCode(ctx, userID) +} + +func (repo *UserRepo) PhoneByID(ctx context.Context, userID string) (*usr_model.Phone, error) { + return repo.UserEvents.PhoneByID(ctx, userID) +} + +func (repo *UserRepo) ChangePhone(ctx context.Context, email *usr_model.Phone) (*usr_model.Phone, error) { + return repo.UserEvents.ChangePhone(ctx, email) +} + +func (repo *UserRepo) CreatePhoneVerificationCode(ctx context.Context, userID string) error { + return repo.UserEvents.CreatePhoneVerificationCode(ctx, userID) +} + +func (repo *UserRepo) AddressByID(ctx context.Context, userID string) (*usr_model.Address, error) { + return repo.UserEvents.AddressByID(ctx, userID) +} + +func (repo *UserRepo) ChangeAddress(ctx context.Context, address *usr_model.Address) (*usr_model.Address, error) { + return repo.UserEvents.ChangeAddress(ctx, address) +} diff --git a/internal/management/repository/eventsourcing/user_grant.go b/internal/management/repository/eventsourcing/user_grant.go new file mode 100644 index 0000000000..ca2915c2c3 --- /dev/null +++ b/internal/management/repository/eventsourcing/user_grant.go @@ -0,0 +1,35 @@ +package eventsourcing + +import ( + "context" + grant_model "github.com/caos/zitadel/internal/usergrant/model" + grant_event "github.com/caos/zitadel/internal/usergrant/repository/eventsourcing" +) + +type UserGrantRepo struct { + UserGrantEvents *grant_event.UserGrantEventStore +} + +func (repo *UserGrantRepo) UserGrantByID(ctx context.Context, grantID string) (*grant_model.UserGrant, error) { + return repo.UserGrantEvents.UserGrantByID(ctx, grantID) +} + +func (repo *UserGrantRepo) AddUserGrant(ctx context.Context, grant *grant_model.UserGrant) (*grant_model.UserGrant, error) { + return repo.UserGrantEvents.AddUserGrant(ctx, grant) +} + +func (repo *UserGrantRepo) ChangeUserGrant(ctx context.Context, grant *grant_model.UserGrant) (*grant_model.UserGrant, error) { + return repo.UserGrantEvents.ChangeUserGrant(ctx, grant) +} + +func (repo *UserGrantRepo) DeactivateUserGrant(ctx context.Context, grantID string) (*grant_model.UserGrant, error) { + return repo.UserGrantEvents.DeactivateUserGrant(ctx, grantID) +} + +func (repo *UserGrantRepo) ReactivateUserGrant(ctx context.Context, grantID string) (*grant_model.UserGrant, error) { + return repo.UserGrantEvents.ReactivateUserGrant(ctx, grantID) +} + +func (repo *UserGrantRepo) RemoveUserGrant(ctx context.Context, grantID string) error { + return repo.UserGrantEvents.RemoveUserGrant(ctx, grantID) +} diff --git a/internal/token/model/token.go b/internal/token/model/token.go new file mode 100644 index 0000000000..bd8e2ff5ed --- /dev/null +++ b/internal/token/model/token.go @@ -0,0 +1,58 @@ +package model + +import ( + "time" + + "github.com/caos/zitadel/internal/model" +) + +type Token struct { + ID string + CreationDate time.Time + ChangeDate time.Time + ResourceOwner string + UserID string + ApplicationID string + UserAgentID string + Expiration time.Time + Sequence uint64 +} + +type TokenSearchRequest struct { + Offset uint64 + Limit uint64 + SortingColumn TokenSearchKey + Asc bool + Queries []*TokenSearchQuery +} + +type TokenSearchKey int32 + +const ( + TOKENSEARCHKEY_UNSPECIFIED TokenSearchKey = iota + TOKENSEARCHKEY_TOKEN_ID + TOKENSEARCHKEY_USER_ID + TOKENSEARCHKEY_APPLICATION_ID + TOKENSEARCHKEY_USER_AGENT_ID + TOKENSEARCHKEY_EXPIRATION + TOKENSEARCHKEY_RESOURCEOWNER +) + +type TokenSearchQuery struct { + Key TokenSearchKey + Method model.SearchMethod + Value string +} + +type TokenSearchResponse struct { + Offset uint64 + Limit uint64 + TotalResult uint64 + Result []*Token +} + +func (r *TokenSearchRequest) EnsureLimit(limit uint64) { + if r.Limit == 0 || r.Limit > limit { + r.Limit = limit + } +} diff --git a/internal/token/repository/view/model/token.go b/internal/token/repository/view/model/token.go new file mode 100644 index 0000000000..2c094322d2 --- /dev/null +++ b/internal/token/repository/view/model/token.go @@ -0,0 +1,56 @@ +package model + +import ( + "time" + + "github.com/caos/zitadel/internal/token/model" +) + +const ( + TokenKeyTokenID = "id" + TokenKeyUserID = "user_id" + TokenKeyApplicationID = "application_id" + TokenKeyUserAgentID = "user_agent_id" + TokenKeyExpiration = "expiration" + TokenKeyResourceOwner = "resource_owner" +) + +type Token struct { + ID string `json:"-" gorm:"column:id;primary_key"` + CreationDate time.Time `json:"-" gorm:"column:creation_date"` + ChangeDate time.Time `json:"-" gorm:"column:change_date"` + ResourceOwner string `json:"-" gorm:"column:resource_owner"` + UserID string `json:"-" gorm:"column:user_id"` + ApplicationID string `json:"-" gorm:"column:application_id"` + UserAgentID string `json:"-" gorm:"column:user_agent_id"` + Expiration time.Time `json:"-" gorm:"column:expiration"` + Sequence uint64 `json:"-" gorm:"column:sequence"` +} + +func TokenFromModel(token *model.Token) *Token { + return &Token{ + ID: token.ID, + CreationDate: token.CreationDate, + ChangeDate: token.ChangeDate, + ResourceOwner: token.ResourceOwner, + UserID: token.UserID, + ApplicationID: token.ApplicationID, + UserAgentID: token.UserAgentID, + Expiration: token.Expiration, + Sequence: token.Sequence, + } +} + +func TokenToModel(token *Token) *model.Token { + return &model.Token{ + ID: token.ID, + CreationDate: token.CreationDate, + ChangeDate: token.ChangeDate, + ResourceOwner: token.ResourceOwner, + UserID: token.UserID, + ApplicationID: token.ApplicationID, + UserAgentID: token.UserAgentID, + Expiration: token.Expiration, + Sequence: token.Sequence, + } +} diff --git a/internal/token/repository/view/model/token_query.go b/internal/token/repository/view/model/token_query.go new file mode 100644 index 0000000000..0867270ac1 --- /dev/null +++ b/internal/token/repository/view/model/token_query.go @@ -0,0 +1,69 @@ +package model + +import ( + global_model "github.com/caos/zitadel/internal/model" + token_model "github.com/caos/zitadel/internal/token/model" + "github.com/caos/zitadel/internal/view" +) + +type TokenSearchRequest token_model.TokenSearchRequest +type TokenSearchQuery token_model.TokenSearchQuery +type TokenSearchKey token_model.TokenSearchKey + +func (req TokenSearchRequest) GetLimit() uint64 { + return req.Limit +} + +func (req TokenSearchRequest) GetOffset() uint64 { + return req.Offset +} + +func (req TokenSearchRequest) GetSortingColumn() view.ColumnKey { + if req.SortingColumn == token_model.TOKENSEARCHKEY_UNSPECIFIED { + return nil + } + return TokenSearchKey(req.SortingColumn) +} + +func (req TokenSearchRequest) GetAsc() bool { + return req.Asc +} + +func (req TokenSearchRequest) GetQueries() []view.SearchQuery { + result := make([]view.SearchQuery, len(req.Queries)) + for i, q := range req.Queries { + result[i] = TokenSearchQuery{Key: q.Key, Value: q.Value, Method: q.Method} + } + return result +} + +func (req TokenSearchQuery) GetKey() view.ColumnKey { + return TokenSearchKey(req.Key) +} + +func (req TokenSearchQuery) GetMethod() global_model.SearchMethod { + return req.Method +} + +func (req TokenSearchQuery) GetValue() interface{} { + return req.Value +} + +func (key TokenSearchKey) ToColumnName() string { + switch token_model.TokenSearchKey(key) { + case token_model.TOKENSEARCHKEY_TOKEN_ID: + return TokenKeyTokenID + case token_model.TOKENSEARCHKEY_USER_AGENT_ID: + return TokenKeyUserAgentID + case token_model.TOKENSEARCHKEY_USER_ID: + return TokenKeyUserID + case token_model.TOKENSEARCHKEY_APPLICATION_ID: + return TokenKeyApplicationID + case token_model.TOKENSEARCHKEY_EXPIRATION: + return TokenKeyExpiration + case token_model.TOKENSEARCHKEY_RESOURCEOWNER: + return TokenKeyResourceOwner + default: + return "" + } +} diff --git a/internal/token/repository/view/token.go b/internal/token/repository/view/token.go new file mode 100644 index 0000000000..ef40ece290 --- /dev/null +++ b/internal/token/repository/view/token.go @@ -0,0 +1,48 @@ +package view + +import ( + "time" + + "github.com/jinzhu/gorm" + + "github.com/caos/zitadel/internal/errors" + token_model "github.com/caos/zitadel/internal/token/model" + "github.com/caos/zitadel/internal/token/repository/view/model" + "github.com/caos/zitadel/internal/view" +) + +func TokenByID(db *gorm.DB, table, tokenID string) (*model.Token, error) { + token := new(model.Token) + query := view.PrepareGetByKey(table, model.TokenSearchKey(token_model.TOKENSEARCHKEY_TOKEN_ID), tokenID) + err := query(db, token) + return token, err +} + +func IsTokenValid(db *gorm.DB, table, tokenID string) (bool, error) { + token, err := TokenByID(db, table, tokenID) + if err == nil { + return token.Expiration.After(time.Now().UTC()), nil + } + if errors.IsNotFound(err) { + return false, nil + } + return false, err +} + +func PutToken(db *gorm.DB, table string, token *model.Token) error { + save := view.PrepareSave(table) + return save(db, token) +} + +func DeleteToken(db *gorm.DB, table, tokenID string) error { + delete := view.PrepareDeleteByKey(table, model.TokenSearchKey(token_model.TOKENSEARCHKEY_TOKEN_ID), tokenID) + return delete(db) +} + +func DeleteTokens(db *gorm.DB, table, agentID, userID string) error { + delete := view.PrepareDeleteByKeys(table, + view.Key{Key: model.TokenSearchKey(token_model.TOKENSEARCHKEY_USER_AGENT_ID), Value: agentID}, + view.Key{Key: model.TokenSearchKey(token_model.TOKENSEARCHKEY_USER_ID), Value: userID}, + ) + return delete(db) +} diff --git a/internal/user/model/user_session_view.go b/internal/user/model/user_session_view.go new file mode 100644 index 0000000000..8e06fc43e3 --- /dev/null +++ b/internal/user/model/user_session_view.go @@ -0,0 +1,61 @@ +package model + +import ( + "time" + + req_model "github.com/caos/zitadel/internal/auth_request/model" + "github.com/caos/zitadel/internal/model" +) + +type UserSessionView struct { + ID string + CreationDate time.Time + ChangeDate time.Time + State req_model.UserSessionState + ResourceOwner string + UserAgentID string + UserID string + UserName string + PasswordVerification time.Time + MfaSoftwareVerification time.Time + MfaHardwareVerification time.Time + Sequence uint64 +} + +type UserSessionSearchRequest struct { + Offset uint64 + Limit uint64 + SortingColumn UserSessionSearchKey + Asc bool + Queries []*UserSessionSearchQuery +} + +type UserSessionSearchKey int32 + +const ( + USERSESSIONSEARCHKEY_UNSPECIFIED UserSessionSearchKey = iota + USERSESSIONSEARCHKEY_SESSION_ID + USERSESSIONSEARCHKEY_USER_AGENT_ID + USERSESSIONSEARCHKEY_USER_ID + USERSESSIONSEARCHKEY_STATE + USERSESSIONSEARCHKEY_RESOURCEOWNER +) + +type UserSessionSearchQuery struct { + Key UserSessionSearchKey + Method model.SearchMethod + Value string +} + +type UserSessionSearchResponse struct { + Offset uint64 + Limit uint64 + TotalResult uint64 + Result []*UserSessionView +} + +func (r *UserSessionSearchRequest) EnsureLimit(limit uint64) { + if r.Limit == 0 || r.Limit > limit { + r.Limit = limit + } +} diff --git a/internal/user/model/user_view.go b/internal/user/model/user_view.go index 6f5d06792d..111d6a071c 100644 --- a/internal/user/model/user_view.go +++ b/internal/user/model/user_view.go @@ -1,36 +1,42 @@ package model import ( - "github.com/caos/zitadel/internal/model" "time" + + req_model "github.com/caos/zitadel/internal/auth_request/model" + "github.com/caos/zitadel/internal/model" ) type UserView struct { - ID string - CreationDate time.Time - ChangeDate time.Time - State UserState - ResourceOwner string - PasswordChanged time.Time - LastLogin time.Time - UserName string - FirstName string - LastName string - NickName string - DisplayName string - PreferredLanguage string - Gender Gender - Email string - IsEmailVerified bool - Phone string - IsPhoneVerified bool - Country string - Locality string - PostalCode string - Region string - StreetAddress string - OTPState MfaState - Sequence uint64 + ID string + CreationDate time.Time + ChangeDate time.Time + State UserState + ResourceOwner string + PasswordSet bool + PasswordChangeRequired bool + PasswordChanged time.Time + LastLogin time.Time + UserName string + FirstName string + LastName string + NickName string + DisplayName string + PreferredLanguage string + Gender Gender + Email string + IsEmailVerified bool + Phone string + IsPhoneVerified bool + Country string + Locality string + PostalCode string + Region string + StreetAddress string + OTPState MfaState + MfaMaxSetUp req_model.MfaLevel + MfaInitSkipped time.Time + Sequence uint64 } type UserSearchRequest struct { @@ -78,3 +84,35 @@ func (r *UserSearchRequest) EnsureLimit(limit uint64) { func (r *UserSearchRequest) AppendMyOrgQuery(orgID string) { r.Queries = append(r.Queries, &UserSearchQuery{Key: USERSEARCHKEY_RESOURCEOWNER, Method: model.SEARCHMETHOD_EQUALS, Value: orgID}) } + +func (u *UserView) MfaTypesSetupPossible(level req_model.MfaLevel) []req_model.MfaType { + types := make([]req_model.MfaType, 0) + switch level { + case req_model.MfaLevelSoftware: + if u.OTPState != MFASTATE_READY { + types = append(types, req_model.MfaTypeOTP) + } + //PLANNED: add sms + fallthrough + case req_model.MfaLevelHardware: + //PLANNED: add token + } + return types +} + +func (u *UserView) MfaTypesAllowed(level req_model.MfaLevel) []req_model.MfaType { + types := make([]req_model.MfaType, 0) + switch level { + default: + fallthrough + case req_model.MfaLevelSoftware: + if u.OTPState == MFASTATE_READY { + types = append(types, req_model.MfaTypeOTP) + } + //PLANNED: add sms + fallthrough + case req_model.MfaLevelHardware: + //PLANNED: add token + } + return types +} diff --git a/internal/user/repository/eventsourcing/eventstore.go b/internal/user/repository/eventsourcing/eventstore.go index ce271d8659..2c614db576 100644 --- a/internal/user/repository/eventsourcing/eventstore.go +++ b/internal/user/repository/eventsourcing/eventstore.go @@ -4,6 +4,10 @@ import ( "context" "strconv" + "github.com/pquerna/otp/totp" + "github.com/sony/sonyflake" + + req_model "github.com/caos/zitadel/internal/auth_request/model" "github.com/caos/zitadel/internal/cache/config" sd "github.com/caos/zitadel/internal/config/systemdefaults" "github.com/caos/zitadel/internal/crypto" @@ -14,8 +18,6 @@ import ( global_model "github.com/caos/zitadel/internal/model" usr_model "github.com/caos/zitadel/internal/user/model" "github.com/caos/zitadel/internal/user/repository/eventsourcing/model" - "github.com/pquerna/otp/totp" - "github.com/sony/sonyflake" ) type UserEventstore struct { @@ -28,6 +30,7 @@ type UserEventstore struct { PhoneVerificationCode crypto.Generator PasswordVerificationCode crypto.Generator Multifactors global_model.Multifactors + validateTOTP func(string, string) bool } type UserConfig struct { @@ -71,6 +74,7 @@ func StartUser(conf UserConfig, systemDefaults sd.SystemDefaults) (*UserEventsto PasswordVerificationCode: passwordVerificationCode, Multifactors: mfa, PasswordAlg: passwordAlg, + validateTOTP: totp.Validate, }, nil } @@ -333,31 +337,80 @@ func (es *UserEventstore) UserPasswordByID(ctx context.Context, userID string) ( return nil, caos_errs.ThrowNotFound(nil, "EVENT-d8e2", "password not found") } -func (es *UserEventstore) SetOneTimePassword(ctx context.Context, password *usr_model.Password) (*usr_model.Password, error) { - return es.changedPassword(ctx, password, true) -} - -func (es *UserEventstore) SetPassword(ctx context.Context, password *usr_model.Password) (*usr_model.Password, error) { - return es.changedPassword(ctx, password, false) -} - -func (es *UserEventstore) changedPassword(ctx context.Context, password *usr_model.Password, onetime bool) (*usr_model.Password, error) { - if !password.IsValid() { - return nil, caos_errs.ThrowPreconditionFailed(nil, "EVENT-dosi3", "password invalid") +func (es *UserEventstore) CheckPassword(ctx context.Context, userID, password string, authRequest *req_model.AuthRequest) error { + existing, err := es.UserByID(ctx, userID) + if err != nil { + return err } + if existing.Password == nil { + return caos_errs.ThrowPreconditionFailed(nil, "EVENT-s35Fa", "no password set") + } + if err := crypto.CompareHash(existing.Password.SecretCrypto, []byte(password), es.PasswordAlg); err == nil { + return es.setPasswordCheckResult(ctx, existing, authRequest, PasswordCheckSucceededAggregate) + } + if err := es.setPasswordCheckResult(ctx, existing, authRequest, PasswordCheckFailedAggregate); err != nil { + return err + } + return caos_errs.ThrowInvalidArgument(nil, "EVENT-452ad", "invalid password") +} + +func (es *UserEventstore) setPasswordCheckResult(ctx context.Context, user *usr_model.User, authRequest *req_model.AuthRequest, check func(*es_models.AggregateCreator, *model.User, *model.AuthRequest) es_sdk.AggregateFunc) error { + repoUser := model.UserFromModel(user) + repoAuthRequest := model.AuthRequestFromModel(authRequest) + agg := check(es.AggregateCreator(), repoUser, repoAuthRequest) + err := es_sdk.Push(ctx, es.PushAggregates, repoUser.AppendEvents, agg) + if err != nil { + return err + } + es.userCache.cacheUser(repoUser) + return nil +} + +func (es *UserEventstore) SetOneTimePassword(ctx context.Context, password *usr_model.Password) (*usr_model.Password, error) { user, err := es.UserByID(ctx, password.AggregateID) if err != nil { return nil, err } + return es.changedPassword(ctx, user, password.SecretString, true) +} - err = password.HashPasswordIfExisting(es.PasswordAlg, onetime) +func (es *UserEventstore) SetPassword(ctx context.Context, userID, code, password string) error { + user, err := es.UserByID(ctx, userID) + if err != nil { + return err + } + if user.PasswordCode == nil { + return caos_errs.ThrowPreconditionFailed(nil, "EVENT-65sdr", "reset code not found") + } + if err := crypto.VerifyCode(user.PasswordCode.CreationDate, user.PasswordCode.Expiry, user.PasswordCode.Code, code, es.PasswordVerificationCode); err != nil { + return caos_errs.ThrowPreconditionFailed(err, "EVENT-sd6DF", "code invalid") + } + _, err = es.changedPassword(ctx, user, password, false) + return err +} + +func (es *UserEventstore) ChangePassword(ctx context.Context, userID, old, new string) (*usr_model.Password, error) { + user, err := es.UserByID(ctx, userID) if err != nil { return nil, err } + if user.Password == nil { + return nil, caos_errs.ThrowPreconditionFailed(nil, "EVENT-Fds3s", "user has no password") + } + if err := crypto.CompareHash(user.Password.SecretCrypto, []byte(old), es.PasswordAlg); err != nil { + return nil, caos_errs.ThrowInvalidArgument(nil, "EVENT-s56a3", "invalid password") + } + return es.changedPassword(ctx, user, new, false) +} +func (es *UserEventstore) changedPassword(ctx context.Context, user *usr_model.User, password string, onetime bool) (*usr_model.Password, error) { + //TODO: check password policy + secret, err := crypto.Hash([]byte(password), es.PasswordAlg) + if err != nil { + return nil, err + } + repoPassword := &model.Password{Secret: secret, ChangeRequired: onetime} repoUser := model.UserFromModel(user) - repoPassword := model.PasswordFromModel(password) - agg := PasswordChangeAggregate(es.AggregateCreator(), repoUser, repoPassword) err = es_sdk.Push(ctx, es.PushAggregates, repoUser.AppendEvents, agg) if err != nil { @@ -666,21 +719,6 @@ func (es *UserEventstore) ChangeAddress(ctx context.Context, address *usr_model. return model.AddressToModel(repoExisting.Address), nil } -func (es *UserEventstore) OTPByID(ctx context.Context, userID string) (*usr_model.OTP, error) { - if userID == "" { - return nil, caos_errs.ThrowPreconditionFailed(nil, "EVENT-do9se", "userID missing") - } - user, err := es.UserByID(ctx, userID) - if err != nil { - return nil, err - } - - if user.OTP != nil { - return user.OTP, nil - } - return nil, caos_errs.ThrowNotFound(nil, "EVENT-dps09", "otp not found") -} - func (es *UserEventstore) AddOTP(ctx context.Context, userID string) (*usr_model.OTP, error) { existing, err := es.UserByID(ctx, userID) if err != nil { @@ -731,22 +769,77 @@ func (es *UserEventstore) RemoveOTP(ctx context.Context, userID string) error { return nil } -func (es *UserEventstore) CheckMfaOTP(ctx context.Context, userID, code string) error { - existing, err := es.UserByID(ctx, userID) +func (es *UserEventstore) CheckMfaOTPSetup(ctx context.Context, userID, code string) error { + user, err := es.UserByID(ctx, userID) if err != nil { return err } - if existing.OTP == nil { - return caos_errs.ThrowPreconditionFailed(nil, "EVENT-sp0de", "no otp existing") + if user.OTP == nil || user.IsOTPReady() { + return caos_errs.ThrowPreconditionFailed(nil, "EVENT-sd5NJ", "otp not existing or already set up") } - decrypt, err := crypto.DecryptString(existing.OTP.Secret, es.Multifactors.OTP.CryptoMFA) + if err := es.verifyMfaOTP(user.OTP, code); err != nil { + return err + } + repoUser := model.UserFromModel(user) + err = es_sdk.Push(ctx, es.PushAggregates, repoUser.AppendEvents, MfaOTPVerifyAggregate(es.AggregateCreator(), repoUser)) if err != nil { return err } - valid := totp.Validate(code, decrypt) + es.userCache.cacheUser(repoUser) + return nil +} + +func (es *UserEventstore) CheckMfaOTP(ctx context.Context, userID, code string, authRequest *req_model.AuthRequest) error { + user, err := es.UserByID(ctx, userID) + if err != nil { + return err + } + if !user.IsOTPReady() { + return caos_errs.ThrowPreconditionFailed(nil, "EVENT-sd5NJ", "opt not ready") + } + + repoUser := model.UserFromModel(user) + repoAuthReq := model.AuthRequestFromModel(authRequest) + var aggregate func(*es_models.AggregateCreator, *model.User, *model.AuthRequest) es_sdk.AggregateFunc + if err := es.verifyMfaOTP(user.OTP, code); err != nil { + aggregate = MfaOTPCheckFailedAggregate + } else { + aggregate = MfaOTPCheckSucceededAggregate + } + err = es_sdk.Push(ctx, es.PushAggregates, repoUser.AppendEvents, aggregate(es.AggregateCreator(), repoUser, repoAuthReq)) + if err != nil { + return err + } + + es.userCache.cacheUser(repoUser) + return nil +} + +func (es *UserEventstore) verifyMfaOTP(otp *usr_model.OTP, code string) error { + decrypt, err := crypto.DecryptString(otp.Secret, es.Multifactors.OTP.CryptoMFA) + if err != nil { + return err + } + + valid := es.validateTOTP(code, decrypt) if !valid { return caos_errs.ThrowInvalidArgument(nil, "EVENT-8isk2", "Invalid code") } return nil } + +func (es *UserEventstore) SignOut(ctx context.Context, agentID, userID string) error { + user, err := es.UserByID(ctx, userID) + if err != nil { + return err + } + repoUser := model.UserFromModel(user) + err = es_sdk.Push(ctx, es.PushAggregates, repoUser.AppendEvents, SignOutAggregate(es.AggregateCreator(), repoUser, agentID)) + if err != nil { + return err + } + + es.userCache.cacheUser(repoUser) + return nil +} diff --git a/internal/user/repository/eventsourcing/eventstore_mock_test.go b/internal/user/repository/eventsourcing/eventstore_mock_test.go index 9eb5cc3fb8..c40960374e 100644 --- a/internal/user/repository/eventsourcing/eventstore_mock_test.go +++ b/internal/user/repository/eventsourcing/eventstore_mock_test.go @@ -2,15 +2,17 @@ package eventsourcing import ( "encoding/json" + "time" + + "github.com/golang/mock/gomock" + "github.com/sony/sonyflake" + mock_cache "github.com/caos/zitadel/internal/cache/mock" "github.com/caos/zitadel/internal/crypto" "github.com/caos/zitadel/internal/eventstore/mock" es_models "github.com/caos/zitadel/internal/eventstore/models" global_model "github.com/caos/zitadel/internal/model" "github.com/caos/zitadel/internal/user/repository/eventsourcing/model" - "github.com/golang/mock/gomock" - "github.com/sony/sonyflake" - "time" ) func GetMockedEventstore(ctrl *gomock.Controller, mockEs *mock.MockEventstore) *UserEventstore { @@ -38,10 +40,7 @@ func GetMockedEventstoreWithPw(ctrl *gomock.Controller, mockEs *mock.MockEventst } if password { es.PasswordVerificationCode = GetMockPwGenerator(ctrl) - hash := crypto.NewMockHashAlgorithm(ctrl) - hash.EXPECT().Hash(gomock.Any()).Return(nil, nil) - hash.EXPECT().Algorithm().Return("bcrypt") - es.PasswordAlg = hash + es.PasswordAlg = crypto.CreateMockHashAlg(ctrl) } return es } @@ -174,8 +173,10 @@ func GetMockManipulateUserWithPhoneCodeGen(ctrl *gomock.Controller, user model.U func GetMockManipulateUserWithPasswordCodeGen(ctrl *gomock.Controller, user model.User) *UserEventstore { data, _ := json.Marshal(user) + code, _ := json.Marshal(user.PasswordCode) events := []*es_models.Event{ &es_models.Event{AggregateID: "AggregateID", Sequence: 1, Type: model.UserAdded, Data: data}, + &es_models.Event{AggregateID: "AggregateID", Sequence: 1, Type: model.UserPasswordCodeAdded, Data: code}, } mockEs := mock.NewMockEventstore(ctrl) mockEs.EXPECT().FilterEvents(gomock.Any(), gomock.Any()).Return(events, nil) @@ -394,29 +395,48 @@ func GetMockManipulateUserFull(ctrl *gomock.Controller) *UserEventstore { return GetMockedEventstore(ctrl, mockEs) } -func GetMockManipulateUserWithOTP(ctrl *gomock.Controller) *UserEventstore { +func GetMockManipulateUserWithOTP(ctrl *gomock.Controller, decrypt, verified bool) *UserEventstore { user := model.User{ Profile: &model.Profile{ UserName: "UserName", }, } - otp := model.OTP{Secret: &crypto.CryptoValue{ - CryptoType: crypto.TypeEncryption, - Algorithm: "enc", - KeyID: "id", - Crypted: []byte("code"), - }} + otp := model.OTP{ + Secret: &crypto.CryptoValue{ + CryptoType: crypto.TypeEncryption, + Algorithm: "enc", + KeyID: "id", + Crypted: []byte("code"), + }, + } dataUser, _ := json.Marshal(user) dataOtp, _ := json.Marshal(otp) events := []*es_models.Event{ &es_models.Event{AggregateID: "AggregateID", Sequence: 1, Type: model.UserAdded, Data: dataUser}, &es_models.Event{AggregateID: "AggregateID", Sequence: 1, Type: model.MfaOtpAdded, Data: dataOtp}, } + if verified { + events = append(events, &es_models.Event{AggregateID: "AggregateID", Sequence: 1, Type: model.MfaOtpVerified}) + } mockEs := mock.NewMockEventstore(ctrl) mockEs.EXPECT().FilterEvents(gomock.Any(), gomock.Any()).Return(events, nil) mockEs.EXPECT().AggregateCreator().Return(es_models.NewAggregateCreator("TEST")) mockEs.EXPECT().PushAggregates(gomock.Any(), gomock.Any()).Return(nil) - return GetMockedEventstore(ctrl, mockEs) + es := GetMockedEventstore(ctrl, mockEs) + if !decrypt { + return es + } + enc := crypto.NewMockEncryptionAlgorithm(ctrl) + enc.EXPECT().Algorithm().Return("enc") + enc.EXPECT().Encrypt(gomock.Any()).Return(nil, nil) + enc.EXPECT().EncryptionKeyID().Return("id") + enc.EXPECT().DecryptionKeyIDs().Return([]string{"id"}) + enc.EXPECT().DecryptString(gomock.Any(), gomock.Any()).Return("code", nil) + es.Multifactors = global_model.Multifactors{OTP: global_model.OTP{ + Issuer: "Issuer", + CryptoMFA: enc, + }} + return es } func GetMockManipulateUserNoEvents(ctrl *gomock.Controller) *UserEventstore { diff --git a/internal/user/repository/eventsourcing/eventstore_test.go b/internal/user/repository/eventsourcing/eventstore_test.go index 5eeea6076c..dacebaf859 100644 --- a/internal/user/repository/eventsourcing/eventstore_test.go +++ b/internal/user/repository/eventsourcing/eventstore_test.go @@ -2,14 +2,19 @@ package eventsourcing import ( "context" + "net" + "testing" + "time" + + "github.com/golang/mock/gomock" + "github.com/caos/zitadel/internal/api/auth" + req_model "github.com/caos/zitadel/internal/auth_request/model" + "github.com/caos/zitadel/internal/crypto" caos_errs "github.com/caos/zitadel/internal/errors" es_models "github.com/caos/zitadel/internal/eventstore/models" "github.com/caos/zitadel/internal/user/model" repo_model "github.com/caos/zitadel/internal/user/repository/eventsourcing/model" - "github.com/golang/mock/gomock" - "testing" - "time" ) func TestUserByID(t *testing.T) { @@ -1025,16 +1030,17 @@ func TestSetOneTimePassword(t *testing.T) { } } -func TestSetPassword(t *testing.T) { +func TestCheckPassword(t *testing.T) { ctrl := gomock.NewController(t) type args struct { - es *UserEventstore - ctx context.Context - password *model.Password + es *UserEventstore + ctx context.Context + userID string + password string + authRequest *req_model.AuthRequest } type res struct { - password *model.Password - errFunc func(err error) bool + errFunc func(err error) bool } tests := []struct { name string @@ -1042,22 +1048,40 @@ func TestSetPassword(t *testing.T) { res res }{ { - name: "create pw", + name: "check pw ok", args: args{ - es: GetMockManipulateUserWithPasswordCodeGen(ctrl, repo_model.User{ObjectRoot: es_models.ObjectRoot{AggregateID: "AggregateID"}}), + es: GetMockManipulateUserWithPasswordAndEmailCodeGen(ctrl, + repo_model.User{ + ObjectRoot: es_models.ObjectRoot{AggregateID: "AggregateID"}, + Password: &repo_model.Password{Secret: &crypto.CryptoValue{ + CryptoType: crypto.TypeHash, + Algorithm: "hash", + Crypted: []byte("password"), + }}, + }, + ), ctx: auth.NewMockContext("orgID", "userID"), - password: &model.Password{ObjectRoot: es_models.ObjectRoot{AggregateID: "AggregateID"}, SecretString: "Password"}, - }, - res: res{ - password: &model.Password{ObjectRoot: es_models.ObjectRoot{AggregateID: "AggregateID", Sequence: 1}, ChangeRequired: false}, + userID: "userID", + password: "password", + authRequest: &req_model.AuthRequest{ + ID: "id", + AgentID: "agentID", + BrowserInfo: &req_model.BrowserInfo{ + UserAgent: "user agent", + AcceptLanguage: "accept langugage", + RemoteIP: net.IPv4(29, 4, 20, 19), + }, + }, }, + res: res{}, }, { name: "empty userid", args: args{ es: GetMockManipulateUser(ctrl), ctx: auth.NewMockContext("orgID", "userID"), - password: &model.Password{ObjectRoot: es_models.ObjectRoot{AggregateID: ""}, SecretString: "Password"}, + userID: "", + password: "password", }, res: res{ errFunc: caos_errs.IsPreconditionFailed, @@ -1068,25 +1092,311 @@ func TestSetPassword(t *testing.T) { args: args{ es: GetMockManipulateUserNoEvents(ctrl), ctx: auth.NewMockContext("orgID", "userID"), - password: &model.Password{ObjectRoot: es_models.ObjectRoot{AggregateID: "AggregateID"}, SecretString: "Password"}, + userID: "userID", + password: "password", }, res: res{ errFunc: caos_errs.IsNotFound, }, }, + { + name: "no password", + args: args{ + es: GetMockUserByIDOK(ctrl, + repo_model.User{ + ObjectRoot: es_models.ObjectRoot{AggregateID: "AggregateID"}, + }, + ), + ctx: auth.NewMockContext("orgID", "userID"), + userID: "userID", + password: "password", + }, + res: res{ + errFunc: caos_errs.IsPreconditionFailed, + }, + }, + { + name: "invalid password", + args: args{ + es: GetMockManipulateUserWithPasswordCodeGen(ctrl, + repo_model.User{ + ObjectRoot: es_models.ObjectRoot{AggregateID: "AggregateID"}, + Password: &repo_model.Password{Secret: &crypto.CryptoValue{ + CryptoType: crypto.TypeHash, + Algorithm: "hash", + Crypted: []byte("password"), + }}, + }, + ), + ctx: auth.NewMockContext("orgID", "userID"), + userID: "userID", + password: "wrong password", + authRequest: &req_model.AuthRequest{ + ID: "id", + AgentID: "agentID", + BrowserInfo: &req_model.BrowserInfo{ + UserAgent: "user agent", + AcceptLanguage: "accept langugage", + RemoteIP: net.IPv4(29, 4, 20, 19), + }, + }, + }, + res: res{ + errFunc: caos_errs.IsErrorInvalidArgument, + }, + }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - result, err := tt.args.es.SetPassword(tt.args.ctx, tt.args.password) + err := tt.args.es.CheckPassword(tt.args.ctx, tt.args.userID, tt.args.password, tt.args.authRequest) + + if tt.res.errFunc == nil && err != nil { + t.Errorf("result has error: %v", err) + } + if tt.res.errFunc != nil && !tt.res.errFunc(err) { + t.Errorf("got wrong err: %v", err) + } + }) + } +} + +func TestSetPassword(t *testing.T) { + ctrl := gomock.NewController(t) + type args struct { + es *UserEventstore + ctx context.Context + userID string + code string + password string + } + type res struct { + errFunc func(err error) bool + } + tests := []struct { + name string + args args + res res + }{ + { + name: "create pw", + args: args{ + es: GetMockManipulateUserWithPasswordCodeGen(ctrl, + repo_model.User{ + ObjectRoot: es_models.ObjectRoot{AggregateID: "AggregateID"}, + PasswordCode: &repo_model.PasswordCode{Code: &crypto.CryptoValue{ + CryptoType: crypto.TypeEncryption, + Algorithm: "enc", + KeyID: "id", + Crypted: []byte("code"), + }}, + }, + ), + ctx: auth.NewMockContext("orgID", "userID"), + userID: "userID", + code: "code", + password: "password", + }, + res: res{}, + }, + { + name: "empty userid", + args: args{ + es: GetMockManipulateUser(ctrl), + ctx: auth.NewMockContext("orgID", "userID"), + userID: "", + code: "code", + password: "password", + }, + res: res{ + errFunc: caos_errs.IsPreconditionFailed, + }, + }, + { + name: "existing user not found", + args: args{ + es: GetMockManipulateUserNoEvents(ctrl), + ctx: auth.NewMockContext("orgID", "userID"), + userID: "userID", + code: "code", + password: "password", + }, + res: res{ + errFunc: caos_errs.IsNotFound, + }, + }, + { + name: "no passcode", + args: args{ + es: GetMockManipulateUserWithPasswordCodeGen(ctrl, + repo_model.User{ + ObjectRoot: es_models.ObjectRoot{AggregateID: "AggregateID"}, + }, + ), + ctx: auth.NewMockContext("orgID", "userID"), + userID: "userID", + code: "code", + password: "password", + }, + res: res{ + errFunc: caos_errs.IsPreconditionFailed, + }, + }, + { + name: "invalid passcode", + args: args{ + es: GetMockManipulateUserWithPasswordCodeGen(ctrl, + repo_model.User{ + ObjectRoot: es_models.ObjectRoot{AggregateID: "AggregateID"}, + PasswordCode: &repo_model.PasswordCode{Code: &crypto.CryptoValue{ + CryptoType: crypto.TypeEncryption, + Algorithm: "enc2", + KeyID: "id", + Crypted: []byte("code2"), + }}, + }, + ), + ctx: auth.NewMockContext("orgID", "userID"), + userID: "userID", + code: "code", + password: "password", + }, + res: res{ + errFunc: caos_errs.IsPreconditionFailed, + }, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + err := tt.args.es.SetPassword(tt.args.ctx, tt.args.userID, tt.args.code, tt.args.password) + + if tt.res.errFunc == nil && err != nil { + t.Errorf("result has error: %v", err) + } + if tt.res.errFunc != nil && !tt.res.errFunc(err) { + t.Errorf("got wrong err: %v", err) + } + }) + } +} + +func TestChangePassword(t *testing.T) { + ctrl := gomock.NewController(t) + type args struct { + es *UserEventstore + ctx context.Context + userID string + old string + new string + } + type res struct { + password string + errFunc func(err error) bool + } + tests := []struct { + name string + args args + res res + }{ + { + name: "change pw", + args: args{ + es: GetMockManipulateUserWithPasswordAndEmailCodeGen(ctrl, + repo_model.User{ + ObjectRoot: es_models.ObjectRoot{AggregateID: "AggregateID"}, + Password: &repo_model.Password{Secret: &crypto.CryptoValue{ + CryptoType: crypto.TypeHash, + Algorithm: "hash", + Crypted: []byte("old"), + }}, + }, + ), + ctx: auth.NewMockContext("orgID", "userID"), + userID: "userID", + old: "old", + new: "new", + }, + res: res{ + password: "new", + }, + }, + { + name: "empty userid", + args: args{ + es: GetMockManipulateUser(ctrl), + ctx: auth.NewMockContext("orgID", "userID"), + userID: "", + old: "old", + new: "new", + }, + res: res{ + errFunc: caos_errs.IsPreconditionFailed, + }, + }, + { + name: "existing user not found", + args: args{ + es: GetMockManipulateUserNoEvents(ctrl), + ctx: auth.NewMockContext("orgID", "userID"), + userID: "userID", + old: "old", + new: "new", + }, + res: res{ + errFunc: caos_errs.IsNotFound, + }, + }, + { + name: "no password", + args: args{ + es: GetMockManipulateUserWithPasswordCodeGen(ctrl, + repo_model.User{ + ObjectRoot: es_models.ObjectRoot{AggregateID: "AggregateID"}, + }, + ), + ctx: auth.NewMockContext("orgID", "userID"), + userID: "userID", + old: "old", + new: "new", + }, + res: res{ + errFunc: caos_errs.IsPreconditionFailed, + }, + }, + { + name: "invalid password", + args: args{ + es: GetMockManipulateUserWithPasswordCodeGen(ctrl, + repo_model.User{ + ObjectRoot: es_models.ObjectRoot{AggregateID: "AggregateID"}, + Password: &repo_model.Password{Secret: &crypto.CryptoValue{ + CryptoType: crypto.TypeHash, + Algorithm: "hash", + Crypted: []byte("older"), + }}, + }, + ), + ctx: auth.NewMockContext("orgID", "userID"), + userID: "userID", + old: "old", + new: "new", + }, + res: res{ + errFunc: caos_errs.IsErrorInvalidArgument, + }, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + result, err := tt.args.es.ChangePassword(tt.args.ctx, tt.args.userID, tt.args.old, tt.args.new) if tt.res.errFunc == nil && result.AggregateID == "" { t.Errorf("result has no id") } - if tt.res.errFunc == nil && result.ChangeRequired != false { - t.Errorf("should not be one time") + if tt.res.errFunc == nil && string(result.SecretCrypto.Crypted) != tt.res.password { + t.Errorf("got wrong result crypted: expected: %v, actual: %v ", tt.res.password, result.SecretString) } if tt.res.errFunc != nil && !tt.res.errFunc(err) { - t.Errorf("got wrong err: %v ", err) + t.Errorf("got wrong err: %v", err) } }) } @@ -2036,69 +2346,6 @@ func TestChangeAddress(t *testing.T) { } } -func TestOTPByID(t *testing.T) { - ctrl := gomock.NewController(t) - type args struct { - es *UserEventstore - ctx context.Context - existing *model.User - } - type res struct { - errFunc func(err error) bool - } - tests := []struct { - name string - args args - res res - }{ - { - name: "get by id, ok", - args: args{ - es: GetMockManipulateUserWithOTP(ctrl), - ctx: auth.NewMockContext("orgID", "userID"), - existing: &model.User{ObjectRoot: es_models.ObjectRoot{AggregateID: "AggregateID", Sequence: 1}}, - }, - }, - { - name: "empty userid", - args: args{ - es: GetMockManipulateUser(ctrl), - ctx: auth.NewMockContext("orgID", "userID"), - existing: &model.User{ObjectRoot: es_models.ObjectRoot{AggregateID: "", Sequence: 1}}, - }, - res: res{ - errFunc: caos_errs.IsPreconditionFailed, - }, - }, - { - name: "existing user not found", - args: args{ - es: GetMockManipulateUserNoEvents(ctrl), - ctx: auth.NewMockContext("orgID", "userID"), - existing: &model.User{ObjectRoot: es_models.ObjectRoot{AggregateID: "AggregateID", Sequence: 1}}, - }, - res: res{ - errFunc: caos_errs.IsNotFound, - }, - }, - } - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - result, err := tt.args.es.OTPByID(tt.args.ctx, tt.args.existing.AggregateID) - - if tt.res.errFunc == nil && result.AggregateID == "" { - t.Errorf("result has no id") - } - if tt.res.errFunc == nil && result == nil { - t.Errorf("got wrong result change required: actual: %v ", result) - } - if tt.res.errFunc != nil && !tt.res.errFunc(err) { - t.Errorf("got wrong err: %v ", err) - } - }) - } -} - func TestAddOTP(t *testing.T) { ctrl := gomock.NewController(t) type args struct { @@ -2168,6 +2415,245 @@ func TestAddOTP(t *testing.T) { } } +func TestCheckMfaOTPSetup(t *testing.T) { + ctrl := gomock.NewController(t) + type args struct { + es *UserEventstore + ctx context.Context + userID string + code string + } + type res struct { + errFunc func(err error) bool + } + tests := []struct { + name string + args args + res res + }{ + { + name: "setup ok", + args: args{ + es: func() *UserEventstore { + es := GetMockManipulateUserWithOTP(ctrl, true, false) + es.validateTOTP = func(string, string) bool { + return true + } + return es + }(), + ctx: auth.NewMockContext("orgID", "userID"), + userID: "id", + code: "code", + }, + res: res{}, + }, + { + name: "wrong code", + args: args{ + es: func() *UserEventstore { + es := GetMockManipulateUserWithOTP(ctrl, true, false) + es.validateTOTP = func(string, string) bool { + return false + } + return es + }(), + ctx: auth.NewMockContext("orgID", "userID"), + userID: "id", + code: "code", + }, + res: res{ + errFunc: caos_errs.IsErrorInvalidArgument, + }, + }, + { + name: "empty userid", + args: args{ + es: GetMockManipulateUser(ctrl), + ctx: auth.NewMockContext("orgID", "userID"), + code: "code", + }, + res: res{ + errFunc: caos_errs.IsPreconditionFailed, + }, + }, + { + name: "empty code", + args: args{ + es: GetMockManipulateUser(ctrl), + ctx: auth.NewMockContext("orgID", "userID"), + userID: "userID", + }, + res: res{ + errFunc: caos_errs.IsPreconditionFailed, + }, + }, + { + name: "existing user not found", + args: args{ + es: GetMockManipulateUserNoEvents(ctrl), + ctx: auth.NewMockContext("orgID", "userID"), + userID: "userID", + code: "code", + }, + res: res{ + errFunc: caos_errs.IsNotFound, + }, + }, + { + name: "user has no otp", + args: args{ + es: GetMockManipulateUser(ctrl), + ctx: auth.NewMockContext("orgID", "userID"), + userID: "userID", + code: "code", + }, + res: res{ + errFunc: caos_errs.IsPreconditionFailed, + }, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + err := tt.args.es.CheckMfaOTPSetup(tt.args.ctx, tt.args.userID, tt.args.code) + + if tt.res.errFunc == nil && err != nil { + t.Errorf("result should not get err") + } + if tt.res.errFunc != nil && !tt.res.errFunc(err) { + t.Errorf("got wrong err: %v ", err) + } + }) + } +} + +func TestCheckMfaOTP(t *testing.T) { + ctrl := gomock.NewController(t) + type args struct { + es *UserEventstore + ctx context.Context + userID string + code string + authRequest *req_model.AuthRequest + } + type res struct { + errFunc func(err error) bool + } + tests := []struct { + name string + args args + res res + }{ + { + name: "check ok", + args: args{ + es: func() *UserEventstore { + es := GetMockManipulateUserWithOTP(ctrl, true, true) + es.validateTOTP = func(string, string) bool { + return true + } + return es + }(), + ctx: auth.NewMockContext("orgID", "userID"), + userID: "id", + code: "code", + authRequest: &req_model.AuthRequest{ + ID: "id", + AgentID: "agentID", + BrowserInfo: &req_model.BrowserInfo{ + UserAgent: "user agent", + AcceptLanguage: "accept langugage", + RemoteIP: net.IPv4(29, 4, 20, 19), + }, + }, + }, + res: res{}, + }, + { + name: "wrong code", + args: args{ + es: func() *UserEventstore { + es := GetMockManipulateUserWithOTP(ctrl, true, true) + es.validateTOTP = func(string, string) bool { + return false + } + return es + }(), + ctx: auth.NewMockContext("orgID", "userID"), + userID: "id", + code: "code", + authRequest: &req_model.AuthRequest{ + ID: "id", + AgentID: "agentID", + BrowserInfo: &req_model.BrowserInfo{ + UserAgent: "user agent", + AcceptLanguage: "accept langugage", + RemoteIP: net.IPv4(29, 4, 20, 19), + }, + }, + }, + res: res{}, + }, + { + name: "empty userid", + args: args{ + es: GetMockManipulateUser(ctrl), + ctx: auth.NewMockContext("orgID", "userID"), + code: "code", + }, + res: res{ + errFunc: caos_errs.IsPreconditionFailed, + }, + }, + { + name: "empty code", + args: args{ + es: GetMockManipulateUser(ctrl), + ctx: auth.NewMockContext("orgID", "userID"), + userID: "userID", + }, + res: res{ + errFunc: caos_errs.IsPreconditionFailed, + }, + }, + { + name: "existing user not found", + args: args{ + es: GetMockManipulateUserNoEvents(ctrl), + ctx: auth.NewMockContext("orgID", "userID"), + userID: "userID", + code: "code", + }, + res: res{ + errFunc: caos_errs.IsNotFound, + }, + }, + { + name: "user has no otp", + args: args{ + es: GetMockManipulateUser(ctrl), + ctx: auth.NewMockContext("orgID", "userID"), + userID: "userID", + code: "code", + }, + res: res{ + errFunc: caos_errs.IsPreconditionFailed, + }, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + err := tt.args.es.CheckMfaOTP(tt.args.ctx, tt.args.userID, tt.args.code, tt.args.authRequest) + + if tt.res.errFunc == nil && err != nil { + t.Errorf("result should not get err, got : %v", err) + } + if tt.res.errFunc != nil && !tt.res.errFunc(err) { + t.Errorf("got wrong err: %v ", err) + } + }) + } +} + func TestRemoveOTP(t *testing.T) { ctrl := gomock.NewController(t) type args struct { @@ -2186,7 +2672,7 @@ func TestRemoveOTP(t *testing.T) { { name: "remove ok", args: args{ - es: GetMockManipulateUserWithOTP(ctrl), + es: GetMockManipulateUserWithOTP(ctrl, false, true), ctx: auth.NewMockContext("orgID", "userID"), existing: &model.User{ObjectRoot: es_models.ObjectRoot{AggregateID: "AggregateID", Sequence: 1}}, }, @@ -2238,80 +2724,3 @@ func TestRemoveOTP(t *testing.T) { }) } } - -func TestCheckOTP(t *testing.T) { - ctrl := gomock.NewController(t) - type args struct { - es *UserEventstore - ctx context.Context - userID string - code string - } - type res struct { - errFunc func(err error) bool - } - tests := []struct { - name string - args args - res res - }{ - { - name: "empty userid", - args: args{ - es: GetMockManipulateUser(ctrl), - ctx: auth.NewMockContext("orgID", "userID"), - code: "code", - }, - res: res{ - errFunc: caos_errs.IsPreconditionFailed, - }, - }, - { - name: "empty code", - args: args{ - es: GetMockManipulateUser(ctrl), - ctx: auth.NewMockContext("orgID", "userID"), - userID: "userID", - }, - res: res{ - errFunc: caos_errs.IsPreconditionFailed, - }, - }, - { - name: "existing user not found", - args: args{ - es: GetMockManipulateUserNoEvents(ctrl), - ctx: auth.NewMockContext("orgID", "userID"), - userID: "userID", - code: "code", - }, - res: res{ - errFunc: caos_errs.IsNotFound, - }, - }, - { - name: "user has no otp", - args: args{ - es: GetMockManipulateUser(ctrl), - ctx: auth.NewMockContext("orgID", "userID"), - userID: "userID", - code: "code", - }, - res: res{ - errFunc: caos_errs.IsPreconditionFailed, - }, - }, - } - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - err := tt.args.es.CheckMfaOTP(tt.args.ctx, tt.args.userID, tt.args.code) - - if tt.res.errFunc == nil && err != nil { - t.Errorf("result should not get err") - } - if tt.res.errFunc != nil && !tt.res.errFunc(err) { - t.Errorf("got wrong err: %v ", err) - } - }) - } -} diff --git a/internal/user/repository/eventsourcing/model/auth_request.go b/internal/user/repository/eventsourcing/model/auth_request.go new file mode 100644 index 0000000000..fc3744517e --- /dev/null +++ b/internal/user/repository/eventsourcing/model/auth_request.go @@ -0,0 +1,35 @@ +package model + +import ( + "net" + + "github.com/caos/zitadel/internal/auth_request/model" +) + +type AuthRequest struct { + ID string `json:"id,omitempty"` + UserAgentID string `json:"userAgentID,omitempty"` + *BrowserInfo +} + +func AuthRequestFromModel(request *model.AuthRequest) *AuthRequest { + return &AuthRequest{ + ID: request.ID, + UserAgentID: request.AgentID, + BrowserInfo: BrowserInfoFromModel(request.BrowserInfo), + } +} + +type BrowserInfo struct { + UserAgent string `json:"userAgent,omitempty"` + AcceptLanguage string `json:"acceptLanguage,omitempty"` + RemoteIP net.IP `json:"remoteIP,omitempty"` +} + +func BrowserInfoFromModel(info *model.BrowserInfo) *BrowserInfo { + return &BrowserInfo{ + UserAgent: info.UserAgent, + AcceptLanguage: info.AcceptLanguage, + RemoteIP: info.RemoteIP, + } +} diff --git a/internal/user/repository/eventsourcing/model/types.go b/internal/user/repository/eventsourcing/model/types.go index d33a4365b0..1ba79757d8 100644 --- a/internal/user/repository/eventsourcing/model/types.go +++ b/internal/user/repository/eventsourcing/model/types.go @@ -23,9 +23,11 @@ const ( UserReactivated models.EventType = "user.reactivated" UserDeleted models.EventType = "user.deleted" - UserPasswordChanged models.EventType = "user.password.changed" - UserPasswordCodeAdded models.EventType = "user.password.code.added" - UserPasswordCodeSent models.EventType = "user.password.code.sent" + UserPasswordChanged models.EventType = "user.password.changed" + UserPasswordCodeAdded models.EventType = "user.password.code.added" + UserPasswordCodeSent models.EventType = "user.password.code.sent" + UserPasswordCheckSucceeded models.EventType = "user.password.check.succeeded" + UserPasswordCheckFailed models.EventType = "user.password.check.failed" UserEmailChanged models.EventType = "user.email.changed" UserEmailVerified models.EventType = "user.email.verified" @@ -40,8 +42,12 @@ const ( UserProfileChanged models.EventType = "user.profile.changed" UserAddressChanged models.EventType = "user.address.changed" - MfaOtpAdded models.EventType = "user.mfa.otp.added" - MfaOtpVerified models.EventType = "user.mfa.otp.verified" - MfaOtpRemoved models.EventType = "user.mfa.otp.removed" - MfaInitSkipped models.EventType = "user.mfa.init.skipped" + MfaOtpAdded models.EventType = "user.mfa.otp.added" + MfaOtpVerified models.EventType = "user.mfa.otp.verified" + MfaOtpRemoved models.EventType = "user.mfa.otp.removed" + MfaOtpCheckSucceeded models.EventType = "user.mfa.otp.check.succeeded" + MfaOtpCheckFailed models.EventType = "user.mfa.otp.check.failed" + MfaInitSkipped models.EventType = "user.mfa.init.skipped" + + SignedOut models.EventType = "user.signed.out" ) diff --git a/internal/user/repository/eventsourcing/user.go b/internal/user/repository/eventsourcing/user.go index 8b35ed230b..7cdb4fffc4 100644 --- a/internal/user/repository/eventsourcing/user.go +++ b/internal/user/repository/eventsourcing/user.go @@ -5,6 +5,7 @@ import ( "github.com/caos/zitadel/internal/errors" es_models "github.com/caos/zitadel/internal/eventstore/models" + es_sdk "github.com/caos/zitadel/internal/eventstore/sdk" "github.com/caos/zitadel/internal/user/repository/eventsourcing/model" ) @@ -167,6 +168,25 @@ func PasswordChangeAggregate(aggCreator *es_models.AggregateCreator, existing *m } } +func PasswordCheckSucceededAggregate(aggCreator *es_models.AggregateCreator, existing *model.User, check *model.AuthRequest) es_sdk.AggregateFunc { + return func(ctx context.Context) (*es_models.Aggregate, error) { + agg, err := UserAggregate(ctx, aggCreator, existing) + if err != nil { + return nil, err + } + return agg.AppendEvent(model.UserPasswordCheckSucceeded, check) + } +} +func PasswordCheckFailedAggregate(aggCreator *es_models.AggregateCreator, existing *model.User, check *model.AuthRequest) es_sdk.AggregateFunc { + return func(ctx context.Context) (*es_models.Aggregate, error) { + agg, err := UserAggregate(ctx, aggCreator, existing) + if err != nil { + return nil, err + } + return agg.AppendEvent(model.UserPasswordCheckFailed, check) + } +} + func RequestSetPassword(aggCreator *es_models.AggregateCreator, existing *model.User, request *model.PasswordCode) func(ctx context.Context) (*es_models.Aggregate, error) { return func(ctx context.Context) (*es_models.Aggregate, error) { if request == nil { @@ -338,6 +358,32 @@ func MfaOTPVerifyAggregate(aggCreator *es_models.AggregateCreator, existing *mod } } +func MfaOTPCheckSucceededAggregate(aggCreator *es_models.AggregateCreator, existing *model.User, authReq *model.AuthRequest) es_sdk.AggregateFunc { + return func(ctx context.Context) (*es_models.Aggregate, error) { + if authReq == nil { + return nil, errors.ThrowPreconditionFailed(nil, "EVENT-sd5DA", "authReq must not be nil") + } + agg, err := UserAggregate(ctx, aggCreator, existing) + if err != nil { + return nil, err + } + return agg.AppendEvent(model.MfaOtpCheckSucceeded, authReq) + } +} + +func MfaOTPCheckFailedAggregate(aggCreator *es_models.AggregateCreator, existing *model.User, authReq *model.AuthRequest) es_sdk.AggregateFunc { + return func(ctx context.Context) (*es_models.Aggregate, error) { + if authReq == nil { + return nil, errors.ThrowPreconditionFailed(nil, "EVENT-64sd6", "authReq must not be nil") + } + agg, err := UserAggregate(ctx, aggCreator, existing) + if err != nil { + return nil, err + } + return agg.AppendEvent(model.MfaOtpCheckFailed, authReq) + } +} + func MfaOTPRemoveAggregate(aggCreator *es_models.AggregateCreator, existing *model.User) func(ctx context.Context) (*es_models.Aggregate, error) { return func(ctx context.Context) (*es_models.Aggregate, error) { agg, err := UserAggregate(ctx, aggCreator, existing) @@ -347,3 +393,13 @@ func MfaOTPRemoveAggregate(aggCreator *es_models.AggregateCreator, existing *mod return agg.AppendEvent(model.MfaOtpRemoved, nil) } } + +func SignOutAggregate(aggCreator *es_models.AggregateCreator, existing *model.User, agentID string) func(ctx context.Context) (*es_models.Aggregate, error) { + return func(ctx context.Context) (*es_models.Aggregate, error) { + agg, err := UserAggregate(ctx, aggCreator, existing) + if err != nil { + return nil, err + } + return agg.AppendEvent(model.SignedOut, map[string]interface{}{"agentID": agentID}) + } +} diff --git a/internal/user/repository/view/model/user.go b/internal/user/repository/view/model/user.go index 3558b4e230..7ed99e0f9c 100644 --- a/internal/user/repository/view/model/user.go +++ b/internal/user/repository/view/model/user.go @@ -2,12 +2,15 @@ package model import ( "encoding/json" + "time" + "github.com/caos/logging" + + req_model "github.com/caos/zitadel/internal/auth_request/model" caos_errs "github.com/caos/zitadel/internal/errors" "github.com/caos/zitadel/internal/eventstore/models" "github.com/caos/zitadel/internal/user/model" es_model "github.com/caos/zitadel/internal/user/repository/eventsourcing/model" - "time" ) const ( @@ -23,90 +26,102 @@ const ( ) type UserView struct { - ID string `json:"-" gorm:"column:id;primary_key"` - CreationDate time.Time `json:"-" gorm:"column:creation_date"` - ChangeDate time.Time `json:"-" gorm:"column:change_date"` - ResourceOwner string `json:"-" gorm:"column:resource_owner"` - State int32 `json:"-" gorm:"column:user_state"` - PasswordChanged time.Time `json:"-" gorm:"column:password_change"` - LastLogin time.Time `json:"-" gorm:"column:last_login"` - UserName string `json:"userName" gorm:"column:user_name"` - FirstName string `json:"firstName" gorm:"column:first_name"` - LastName string `json:"lastName" gorm:"column:last_name"` - NickName string `json:"nickName" gorm:"column:nick_name"` - DisplayName string `json:"displayName" gorm:"column:display_name"` - PreferredLanguage string `json:"preferredLanguage" gorm:"column:preferred_language"` - Gender int32 `json:"gender" gorm:"column:gender"` - Email string `json:"email" gorm:"column:email"` - IsEmailVerified bool `json:"-" gorm:"column:is_email_verified"` - Phone string `json:"phone" gorm:"column:phone"` - IsPhoneVerified bool `json:"-" gorm:"column:is_phone_verified"` - Country string `json:"country" gorm:"column:country"` - Locality string `json:"locality" gorm:"column:locality"` - PostalCode string `json:"postalCode" gorm:"column:postal_code"` - Region string `json:"region" gorm:"column:region"` - StreetAddress string `json:"streetAddress" gorm:"column:street_address"` - OTPState int32 `json:"-" gorm:"column:otp_state"` - Sequence uint64 `json:"-" gorm:"column:sequence"` + ID string `json:"-" gorm:"column:id;primary_key"` + CreationDate time.Time `json:"-" gorm:"column:creation_date"` + ChangeDate time.Time `json:"-" gorm:"column:change_date"` + ResourceOwner string `json:"-" gorm:"column:resource_owner"` + State int32 `json:"-" gorm:"column:user_state"` + PasswordSet bool `json:"-" gorm:"column:password_set"` + PasswordChangeRequired bool `json:"-" gorm:"column:password_change_required"` + PasswordChanged time.Time `json:"-" gorm:"column:password_change"` + LastLogin time.Time `json:"-" gorm:"column:last_login"` + UserName string `json:"userName" gorm:"column:user_name"` + FirstName string `json:"firstName" gorm:"column:first_name"` + LastName string `json:"lastName" gorm:"column:last_name"` + NickName string `json:"nickName" gorm:"column:nick_name"` + DisplayName string `json:"displayName" gorm:"column:display_name"` + PreferredLanguage string `json:"preferredLanguage" gorm:"column:preferred_language"` + Gender int32 `json:"gender" gorm:"column:gender"` + Email string `json:"email" gorm:"column:email"` + IsEmailVerified bool `json:"-" gorm:"column:is_email_verified"` + Phone string `json:"phone" gorm:"column:phone"` + IsPhoneVerified bool `json:"-" gorm:"column:is_phone_verified"` + Country string `json:"country" gorm:"column:country"` + Locality string `json:"locality" gorm:"column:locality"` + PostalCode string `json:"postalCode" gorm:"column:postal_code"` + Region string `json:"region" gorm:"column:region"` + StreetAddress string `json:"streetAddress" gorm:"column:street_address"` + OTPState int32 `json:"-" gorm:"column:otp_state"` + MfaMaxSetUp int32 `json:"-" gorm:"column:mfa_max_set_up"` + MfaInitSkipped time.Time `json:"-" gorm:"column:mfa_init_skipped"` + Sequence uint64 `json:"-" gorm:"column:sequence"` } func UserFromModel(user *model.UserView) *UserView { return &UserView{ - ID: user.ID, - ChangeDate: user.ChangeDate, - CreationDate: user.CreationDate, - ResourceOwner: user.ResourceOwner, - State: int32(user.State), - PasswordChanged: user.PasswordChanged, - LastLogin: user.LastLogin, - UserName: user.UserName, - FirstName: user.FirstName, - LastName: user.LastName, - NickName: user.NickName, - DisplayName: user.DisplayName, - PreferredLanguage: user.PreferredLanguage, - Gender: int32(user.Gender), - Email: user.Email, - IsEmailVerified: user.IsEmailVerified, - Phone: user.Phone, - IsPhoneVerified: user.IsPhoneVerified, - Country: user.Country, - Locality: user.Locality, - PostalCode: user.PostalCode, - Region: user.Region, - StreetAddress: user.StreetAddress, - OTPState: int32(user.OTPState), - Sequence: user.Sequence, + ID: user.ID, + ChangeDate: user.ChangeDate, + CreationDate: user.CreationDate, + ResourceOwner: user.ResourceOwner, + State: int32(user.State), + PasswordSet: user.PasswordSet, + PasswordChangeRequired: user.PasswordChangeRequired, + PasswordChanged: user.PasswordChanged, + LastLogin: user.LastLogin, + UserName: user.UserName, + FirstName: user.FirstName, + LastName: user.LastName, + NickName: user.NickName, + DisplayName: user.DisplayName, + PreferredLanguage: user.PreferredLanguage, + Gender: int32(user.Gender), + Email: user.Email, + IsEmailVerified: user.IsEmailVerified, + Phone: user.Phone, + IsPhoneVerified: user.IsPhoneVerified, + Country: user.Country, + Locality: user.Locality, + PostalCode: user.PostalCode, + Region: user.Region, + StreetAddress: user.StreetAddress, + OTPState: int32(user.OTPState), + MfaMaxSetUp: int32(user.MfaMaxSetUp), + MfaInitSkipped: user.MfaInitSkipped, + Sequence: user.Sequence, } } func UserToModel(user *UserView) *model.UserView { return &model.UserView{ - ID: user.ID, - ChangeDate: user.ChangeDate, - CreationDate: user.CreationDate, - ResourceOwner: user.ResourceOwner, - State: model.UserState(user.State), - PasswordChanged: user.PasswordChanged, - LastLogin: user.LastLogin, - UserName: user.UserName, - FirstName: user.FirstName, - LastName: user.LastName, - NickName: user.NickName, - DisplayName: user.DisplayName, - PreferredLanguage: user.PreferredLanguage, - Gender: model.Gender(user.Gender), - Email: user.Email, - IsEmailVerified: user.IsEmailVerified, - Phone: user.Phone, - IsPhoneVerified: user.IsPhoneVerified, - Country: user.Country, - Locality: user.Locality, - PostalCode: user.PostalCode, - Region: user.Region, - StreetAddress: user.StreetAddress, - OTPState: model.MfaState(user.OTPState), - Sequence: user.Sequence, + ID: user.ID, + ChangeDate: user.ChangeDate, + CreationDate: user.CreationDate, + ResourceOwner: user.ResourceOwner, + State: model.UserState(user.State), + PasswordSet: user.PasswordSet, + PasswordChangeRequired: user.PasswordChangeRequired, + PasswordChanged: user.PasswordChanged, + LastLogin: user.LastLogin, + UserName: user.UserName, + FirstName: user.FirstName, + LastName: user.LastName, + NickName: user.NickName, + DisplayName: user.DisplayName, + PreferredLanguage: user.PreferredLanguage, + Gender: model.Gender(user.Gender), + Email: user.Email, + IsEmailVerified: user.IsEmailVerified, + Phone: user.Phone, + IsPhoneVerified: user.IsPhoneVerified, + Country: user.Country, + Locality: user.Locality, + PostalCode: user.PostalCode, + Region: user.Region, + StreetAddress: user.StreetAddress, + OTPState: model.MfaState(user.OTPState), + MfaMaxSetUp: req_model.MfaLevel(user.MfaMaxSetUp), + MfaInitSkipped: user.MfaInitSkipped, + Sequence: user.Sequence, } } @@ -118,43 +133,52 @@ func UsersToModel(users []*UserView) []*model.UserView { return result } -func (p *UserView) AppendEvent(event *models.Event) (err error) { - p.ChangeDate = event.CreationDate - p.Sequence = event.Sequence +func (u *UserView) AppendEvent(event *models.Event) (err error) { + u.ChangeDate = event.CreationDate + u.Sequence = event.Sequence switch event.Type { case es_model.UserAdded, es_model.UserRegistered: - p.CreationDate = event.CreationDate - p.setRootData(event) - err = p.setData(event) + u.CreationDate = event.CreationDate + u.setRootData(event) + err = u.setData(event) + if err != nil { + return err + } + err = u.setPasswordData(event) + case es_model.UserPasswordChanged: + err = u.setPasswordData(event) case es_model.UserProfileChanged, es_model.UserAddressChanged: - err = p.setData(event) + err = u.setData(event) case es_model.UserEmailChanged: - p.IsEmailVerified = false - err = p.setData(event) + u.IsEmailVerified = false + err = u.setData(event) case es_model.UserEmailVerified: - p.IsEmailVerified = true + u.IsEmailVerified = true case es_model.UserPhoneChanged: - p.IsPhoneVerified = false - err = p.setData(event) + u.IsPhoneVerified = false + err = u.setData(event) case es_model.UserPhoneVerified: - p.IsPhoneVerified = true + u.IsPhoneVerified = true case es_model.UserDeactivated: - p.State = int32(model.USERSTATE_INACTIVE) + u.State = int32(model.USERSTATE_INACTIVE) case es_model.UserReactivated, es_model.UserUnlocked: - p.State = int32(model.USERSTATE_ACTIVE) + u.State = int32(model.USERSTATE_ACTIVE) case es_model.UserLocked: - p.State = int32(model.USERSTATE_LOCKED) + u.State = int32(model.USERSTATE_LOCKED) case es_model.MfaOtpAdded: - p.OTPState = int32(model.MFASTATE_NOTREADY) + u.OTPState = int32(model.MFASTATE_NOTREADY) case es_model.MfaOtpVerified: - p.OTPState = int32(model.MFASTATE_READY) + u.OTPState = int32(model.MFASTATE_READY) + u.MfaInitSkipped = time.Time{} case es_model.MfaOtpRemoved: - p.OTPState = int32(model.MFASTATE_UNSPECIFIED) + u.OTPState = int32(model.MFASTATE_UNSPECIFIED) + case es_model.MfaInitSkipped: + u.MfaInitSkipped = event.CreationDate } - p.ComputeObject() + u.ComputeObject() return err } @@ -165,12 +189,23 @@ func (u *UserView) setRootData(event *models.Event) { func (u *UserView) setData(event *models.Event) error { if err := json.Unmarshal(event.Data, u); err != nil { - logging.Log("EVEN-lso9e").WithError(err).Error("could not unmarshal event data") + logging.Log("MODEL-lso9e").WithError(err).Error("could not unmarshal event data") return caos_errs.ThrowInternal(nil, "MODEL-8iows", "could not unmarshal data") } return nil } +func (u *UserView) setPasswordData(event *models.Event) error { + password := new(es_model.Password) + if err := json.Unmarshal(event.Data, password); err != nil { + logging.Log("MODEL-sdw4r").WithError(err).Error("could not unmarshal event data") + return caos_errs.ThrowInternal(nil, "MODEL-6jhsw", "could not unmarshal data") + } + u.PasswordSet = password.Secret != nil + u.PasswordChangeRequired = password.ChangeRequired + return nil +} + func (u *UserView) ComputeObject() { if u.State == int32(model.USERSTATE_UNSPECIFIED) || u.State == int32(model.USERSTATE_INITIAL) { if u.IsEmailVerified { @@ -179,4 +214,7 @@ func (u *UserView) ComputeObject() { u.State = int32(model.USERSTATE_INITIAL) } } + if u.OTPState == int32(model.MFASTATE_READY) { + u.MfaMaxSetUp = int32(req_model.MfaLevelSoftware) + } } diff --git a/internal/user/repository/view/model/user_session.go b/internal/user/repository/view/model/user_session.go new file mode 100644 index 0000000000..f4a1e6158f --- /dev/null +++ b/internal/user/repository/view/model/user_session.go @@ -0,0 +1,91 @@ +package model + +import ( + "encoding/json" + "time" + + "github.com/caos/logging" + + req_model "github.com/caos/zitadel/internal/auth_request/model" + caos_errs "github.com/caos/zitadel/internal/errors" + "github.com/caos/zitadel/internal/eventstore/models" + "github.com/caos/zitadel/internal/user/model" + es_model "github.com/caos/zitadel/internal/user/repository/eventsourcing/model" +) + +const ( + UserSessionKeySessionID = "id" + UserSessionKeyUserAgentID = "user_agent_id" + UserSessionKeyUserID = "user_id" + UserSessionKeyState = "state" + UserSessionKeyResourceOwner = "resource_owner" +) + +type UserSessionView struct { + ID string `json:"-" gorm:"column:id;primary_key"` + CreationDate time.Time `json:"-" gorm:"column:creation_date"` + ChangeDate time.Time `json:"-" gorm:"column:change_date"` + ResourceOwner string `json:"-" gorm:"column:resource_owner"` + State int32 `json:"-" gorm:"column:state"` + UserAgentID string `json:"userAgentID" gorm:"column:user_agent_id"` + UserID string `json:"userID" gorm:"column:user_id"` + UserName string `json:"userName" gorm:"column:user_name"` + PasswordVerification time.Time `json:"-" gorm:"column:password_verification"` + MfaSoftwareVerification time.Time `json:"-" gorm:"column:mfa_software_verification"` + MfaHardwareVerification time.Time `json:"-" gorm:"column:mfa_hardware_verification"` + Sequence uint64 `json:"-" gorm:"column:sequence"` +} + +func UserSessionFromEvent(event *models.Event) (*UserSessionView, error) { + v := new(UserSessionView) + if err := json.Unmarshal(event.Data, v); err != nil { + logging.Log("EVEN-lso9e").WithError(err).Error("could not unmarshal event data") + return nil, caos_errs.ThrowInternal(nil, "MODEL-sd325", "could not unmarshal data") + } + return v, nil +} + +func UserSessionToModel(userSession *UserSessionView) *model.UserSessionView { + return &model.UserSessionView{ + ID: userSession.ID, + ChangeDate: userSession.ChangeDate, + CreationDate: userSession.CreationDate, + ResourceOwner: userSession.ResourceOwner, + State: req_model.UserSessionState(userSession.State), + UserAgentID: userSession.UserAgentID, + UserID: userSession.UserID, + UserName: userSession.UserName, + PasswordVerification: userSession.PasswordVerification, + MfaSoftwareVerification: userSession.MfaSoftwareVerification, + MfaHardwareVerification: userSession.MfaHardwareVerification, + Sequence: userSession.Sequence, + } +} + +func UserSessionsToModel(userSessions []*UserSessionView) []*model.UserSessionView { + result := make([]*model.UserSessionView, len(userSessions)) + for i, s := range userSessions { + result[i] = UserSessionToModel(s) + } + return result +} + +func (v *UserSessionView) AppendEvent(event *models.Event) { + v.ChangeDate = event.CreationDate + switch event.Type { + case es_model.UserPasswordCheckSucceeded: + v.PasswordVerification = event.CreationDate + case es_model.UserPasswordCheckFailed, + es_model.UserPasswordChanged: + v.PasswordVerification = time.Time{} + case es_model.MfaOtpCheckSucceeded: + v.MfaSoftwareVerification = event.CreationDate + case es_model.MfaOtpCheckFailed, + es_model.MfaOtpRemoved: + v.MfaSoftwareVerification = time.Time{} + case es_model.SignedOut: + v.PasswordVerification = time.Time{} + v.MfaSoftwareVerification = time.Time{} + v.State = int32(req_model.UserSessionStateTerminated) + } +} diff --git a/internal/user/repository/view/model/user_session_query.go b/internal/user/repository/view/model/user_session_query.go new file mode 100644 index 0000000000..ca7fef4733 --- /dev/null +++ b/internal/user/repository/view/model/user_session_query.go @@ -0,0 +1,67 @@ +package model + +import ( + global_model "github.com/caos/zitadel/internal/model" + usr_model "github.com/caos/zitadel/internal/user/model" + "github.com/caos/zitadel/internal/view" +) + +type UserSessionSearchRequest usr_model.UserSessionSearchRequest +type UserSessionSearchQuery usr_model.UserSessionSearchQuery +type UserSessionSearchKey usr_model.UserSessionSearchKey + +func (req UserSessionSearchRequest) GetLimit() uint64 { + return req.Limit +} + +func (req UserSessionSearchRequest) GetOffset() uint64 { + return req.Offset +} + +func (req UserSessionSearchRequest) GetSortingColumn() view.ColumnKey { + if req.SortingColumn == usr_model.USERSESSIONSEARCHKEY_UNSPECIFIED { + return nil + } + return UserSessionSearchKey(req.SortingColumn) +} + +func (req UserSessionSearchRequest) GetAsc() bool { + return req.Asc +} + +func (req UserSessionSearchRequest) GetQueries() []view.SearchQuery { + result := make([]view.SearchQuery, len(req.Queries)) + for i, q := range req.Queries { + result[i] = UserSessionSearchQuery{Key: q.Key, Value: q.Value, Method: q.Method} + } + return result +} + +func (req UserSessionSearchQuery) GetKey() view.ColumnKey { + return UserSessionSearchKey(req.Key) +} + +func (req UserSessionSearchQuery) GetMethod() global_model.SearchMethod { + return req.Method +} + +func (req UserSessionSearchQuery) GetValue() interface{} { + return req.Value +} + +func (key UserSessionSearchKey) ToColumnName() string { + switch usr_model.UserSessionSearchKey(key) { + case usr_model.USERSESSIONSEARCHKEY_SESSION_ID: + return UserSessionKeySessionID + case usr_model.USERSESSIONSEARCHKEY_USER_AGENT_ID: + return UserSessionKeyUserAgentID + case usr_model.USERSESSIONSEARCHKEY_USER_ID: + return UserSessionKeyUserID + case usr_model.USERSESSIONSEARCHKEY_STATE: + return UserSessionKeyState + case usr_model.USERSESSIONSEARCHKEY_RESOURCEOWNER: + return UserSessionKeyResourceOwner + default: + return "" + } +} diff --git a/internal/user/repository/view/model/user_session_test.go b/internal/user/repository/view/model/user_session_test.go new file mode 100644 index 0000000000..0e936fd16e --- /dev/null +++ b/internal/user/repository/view/model/user_session_test.go @@ -0,0 +1,90 @@ +package model + +import ( + "testing" + "time" + + "github.com/stretchr/testify/assert" + + es_models "github.com/caos/zitadel/internal/eventstore/models" + es_model "github.com/caos/zitadel/internal/user/repository/eventsourcing/model" +) + +func now() time.Time { + return time.Now().UTC().Round(1 * time.Second) +} + +func TestAppendEvent(t *testing.T) { + type args struct { + event *es_models.Event + userView *UserSessionView + } + tests := []struct { + name string + args args + result *UserSessionView + }{ + { + name: "append password check succeeded event", + args: args{ + event: &es_models.Event{CreationDate: now(), Type: es_model.UserPasswordCheckSucceeded}, + userView: &UserSessionView{}, + }, + result: &UserSessionView{ChangeDate: now(), PasswordVerification: now()}, + }, + { + name: "append password check failed event", + args: args{ + event: &es_models.Event{CreationDate: now(), Type: es_model.UserPasswordCheckFailed}, + userView: &UserSessionView{PasswordVerification: now()}, + }, + result: &UserSessionView{ChangeDate: now(), PasswordVerification: time.Time{}}, + }, + { + name: "append password changed event", + args: args{ + event: &es_models.Event{CreationDate: now(), Type: es_model.UserPasswordChanged}, + userView: &UserSessionView{PasswordVerification: now()}, + }, + result: &UserSessionView{ChangeDate: now(), PasswordVerification: time.Time{}}, + }, + { + name: "append otp check succeeded event", + args: args{ + event: &es_models.Event{CreationDate: now(), Type: es_model.MfaOtpCheckSucceeded}, + userView: &UserSessionView{}, + }, + result: &UserSessionView{ChangeDate: now(), MfaSoftwareVerification: now()}, + }, + { + name: "append otp check failed event", + args: args{ + event: &es_models.Event{CreationDate: now(), Type: es_model.MfaOtpCheckFailed}, + userView: &UserSessionView{MfaSoftwareVerification: now()}, + }, + result: &UserSessionView{ChangeDate: now(), MfaSoftwareVerification: time.Time{}}, + }, + { + name: "append otp removed event", + args: args{ + event: &es_models.Event{CreationDate: now(), Type: es_model.MfaOtpCheckFailed}, + userView: &UserSessionView{MfaSoftwareVerification: now()}, + }, + result: &UserSessionView{ChangeDate: now(), MfaSoftwareVerification: time.Time{}}, + }, + { + name: "append otp removed event", + args: args{ + event: &es_models.Event{CreationDate: now(), Type: es_model.SignedOut}, + userView: &UserSessionView{PasswordVerification: now(), MfaSoftwareVerification: now()}, + }, + result: &UserSessionView{ChangeDate: now(), PasswordVerification: time.Time{}, MfaSoftwareVerification: time.Time{}, State: 1}, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + tt.args.userView.AppendEvent(tt.args.event) + assert.Equal(t, tt.result, tt.args.userView) + }) + } +} diff --git a/internal/user/repository/view/model/user_test.go b/internal/user/repository/view/model/user_test.go index 44d2f8020d..f7fae956e1 100644 --- a/internal/user/repository/view/model/user_test.go +++ b/internal/user/repository/view/model/user_test.go @@ -2,10 +2,13 @@ package model import ( "encoding/json" + "testing" + "time" + + "github.com/caos/zitadel/internal/crypto" es_models "github.com/caos/zitadel/internal/eventstore/models" "github.com/caos/zitadel/internal/user/model" es_model "github.com/caos/zitadel/internal/user/repository/eventsourcing/model" - "testing" ) func mockUserData(user *es_model.User) []byte { @@ -13,6 +16,11 @@ func mockUserData(user *es_model.User) []byte { return data } +func mockPasswordData(password *es_model.Password) []byte { + data, _ := json.Marshal(password) + return data +} + func mockProfileData(profile *es_model.Profile) []byte { data, _ := json.Marshal(profile) return data @@ -33,7 +41,7 @@ func mockAddressData(address *es_model.Address) []byte { return data } -func getFullUser() *es_model.User { +func getFullUser(password *es_model.Password) *es_model.User { return &es_model.User{ Profile: &es_model.Profile{ UserName: "UserName", @@ -49,6 +57,7 @@ func getFullUser() *es_model.User { Address: &es_model.Address{ Country: "Country", }, + Password: password, } } @@ -65,11 +74,43 @@ func TestUserAppendEvent(t *testing.T) { { name: "append added user event", args: args{ - event: &es_models.Event{AggregateID: "AggregateID", Sequence: 1, Type: es_model.UserAdded, ResourceOwner: "OrgID", Data: mockUserData(getFullUser())}, + event: &es_models.Event{AggregateID: "AggregateID", Sequence: 1, Type: es_model.UserAdded, ResourceOwner: "OrgID", Data: mockUserData(getFullUser(nil))}, user: &UserView{}, }, result: &UserView{ID: "AggregateID", ResourceOwner: "OrgID", UserName: "UserName", FirstName: "FirstName", LastName: "LastName", Email: "Email", Phone: "Phone", Country: "Country", State: int32(model.USERSTATE_INITIAL)}, }, + { + name: "append added user with password event", + args: args{ + event: &es_models.Event{AggregateID: "AggregateID", Sequence: 1, Type: es_model.UserAdded, ResourceOwner: "OrgID", Data: mockUserData(getFullUser(&es_model.Password{Secret: &crypto.CryptoValue{}}))}, + user: &UserView{}, + }, + result: &UserView{ID: "AggregateID", ResourceOwner: "OrgID", UserName: "UserName", FirstName: "FirstName", LastName: "LastName", Email: "Email", Phone: "Phone", Country: "Country", State: int32(model.USERSTATE_INITIAL), PasswordSet: true}, + }, + { + name: "append added user with password but change required event", + args: args{ + event: &es_models.Event{AggregateID: "AggregateID", Sequence: 1, Type: es_model.UserAdded, ResourceOwner: "OrgID", Data: mockUserData(getFullUser(&es_model.Password{ChangeRequired: true, Secret: &crypto.CryptoValue{}}))}, + user: &UserView{}, + }, + result: &UserView{ID: "AggregateID", ResourceOwner: "OrgID", UserName: "UserName", FirstName: "FirstName", LastName: "LastName", Email: "Email", Phone: "Phone", Country: "Country", State: int32(model.USERSTATE_INITIAL), PasswordSet: true, PasswordChangeRequired: true}, + }, + { + name: "append password change event", + args: args{ + event: &es_models.Event{AggregateID: "AggregateID", Sequence: 1, Type: es_model.UserPasswordChanged, ResourceOwner: "OrgID", Data: mockPasswordData(&es_model.Password{Secret: &crypto.CryptoValue{}})}, + user: &UserView{ID: "AggregateID", ResourceOwner: "OrgID", UserName: "UserName", FirstName: "FirstName", LastName: "LastName", Email: "Email", IsEmailVerified: true, Phone: "Phone", Country: "Country", State: int32(model.USERSTATE_ACTIVE)}, + }, + result: &UserView{ID: "AggregateID", ResourceOwner: "OrgID", UserName: "UserName", FirstName: "FirstName", LastName: "LastName", Email: "Email", IsEmailVerified: true, Phone: "Phone", Country: "Country", State: int32(model.USERSTATE_ACTIVE), PasswordSet: true}, + }, + { + name: "append password change with change required event", + args: args{ + event: &es_models.Event{AggregateID: "AggregateID", Sequence: 1, Type: es_model.UserPasswordChanged, ResourceOwner: "OrgID", Data: mockPasswordData(&es_model.Password{ChangeRequired: true, Secret: &crypto.CryptoValue{}})}, + user: &UserView{ID: "AggregateID", ResourceOwner: "OrgID", UserName: "UserName", FirstName: "FirstName", LastName: "LastName", Email: "Email", IsEmailVerified: true, Phone: "Phone", Country: "Country", State: int32(model.USERSTATE_ACTIVE)}, + }, + result: &UserView{ID: "AggregateID", ResourceOwner: "OrgID", UserName: "UserName", FirstName: "FirstName", LastName: "LastName", Email: "Email", IsEmailVerified: true, Phone: "Phone", Country: "Country", State: int32(model.USERSTATE_ACTIVE), PasswordSet: true, PasswordChangeRequired: true}, + }, { name: "append change user profile event", args: args{ @@ -174,6 +215,14 @@ func TestUserAppendEvent(t *testing.T) { }, result: &UserView{ID: "AggregateID", ResourceOwner: "OrgID", UserName: "UserName", FirstName: "FirstName", LastName: "LastName", Email: "Email", Phone: "Phone", Country: "Country", State: int32(model.USERSTATE_ACTIVE), OTPState: int32(model.MFASTATE_UNSPECIFIED)}, }, + { + name: "append mfa init skipped event", + args: args{ + event: &es_models.Event{Sequence: 1, CreationDate: time.Now().UTC(), Type: es_model.MfaInitSkipped, AggregateID: "AggregateID", ResourceOwner: "OrgID"}, + user: &UserView{ID: "AggregateID", ResourceOwner: "OrgID", UserName: "UserName", FirstName: "FirstName", LastName: "LastName", Email: "Email", Phone: "Phone", Country: "Country", State: int32(model.USERSTATE_ACTIVE)}, + }, + result: &UserView{ID: "AggregateID", ResourceOwner: "OrgID", UserName: "UserName", FirstName: "FirstName", LastName: "LastName", Email: "Email", Phone: "Phone", Country: "Country", State: int32(model.USERSTATE_ACTIVE), MfaInitSkipped: time.Now().UTC()}, + }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { @@ -211,6 +260,15 @@ func TestUserAppendEvent(t *testing.T) { if tt.args.user.OTPState != tt.result.OTPState { t.Errorf("got wrong result OTPState: expected: %v, actual: %v ", tt.result.OTPState, tt.args.user.OTPState) } + if tt.args.user.MfaInitSkipped.Round(1*time.Second) != tt.result.MfaInitSkipped.Round(1*time.Second) { + t.Errorf("got wrong result MfaInitSkipped: expected: %v, actual: %v ", tt.result.MfaInitSkipped.Round(1*time.Second), tt.args.user.MfaInitSkipped.Round(1*time.Second)) + } + if tt.args.user.PasswordSet != tt.result.PasswordSet { + t.Errorf("got wrong result PasswordSet: expected: %v, actual: %v ", tt.result.PasswordSet, tt.args.user.PasswordSet) + } + if tt.args.user.PasswordChangeRequired != tt.result.PasswordChangeRequired { + t.Errorf("got wrong result PasswordChangeRequired: expected: %v, actual: %v ", tt.result.PasswordChangeRequired, tt.args.user.PasswordChangeRequired) + } }) } } diff --git a/internal/user/repository/view/user_session_view.go b/internal/user/repository/view/user_session_view.go new file mode 100644 index 0000000000..652ba6d40a --- /dev/null +++ b/internal/user/repository/view/user_session_view.go @@ -0,0 +1,58 @@ +package view + +import ( + "github.com/jinzhu/gorm" + + global_model "github.com/caos/zitadel/internal/model" + usr_model "github.com/caos/zitadel/internal/user/model" + "github.com/caos/zitadel/internal/user/repository/view/model" + "github.com/caos/zitadel/internal/view" +) + +func UserSessionByID(db *gorm.DB, table, sessionID string) (*model.UserSessionView, error) { + userSession := new(model.UserSessionView) + query := view.PrepareGetByKey(table, model.UserSessionSearchKey(usr_model.USERSESSIONSEARCHKEY_SESSION_ID), sessionID) + err := query(db, userSession) + return userSession, err +} + +func UserSessionByIDs(db *gorm.DB, table, agentID, userID string) (*model.UserSessionView, error) { + userSession := new(model.UserSessionView) + userAgentQuery := model.UserSessionSearchQuery{ + Key: usr_model.USERSESSIONSEARCHKEY_USER_AGENT_ID, + Method: global_model.SEARCHMETHOD_EQUALS, + Value: agentID, + } + userQuery := model.UserSessionSearchQuery{ + Key: usr_model.USERSESSIONSEARCHKEY_USER_ID, + Method: global_model.SEARCHMETHOD_EQUALS, + Value: userID, + } + query := view.PrepareGetByQuery(table, userAgentQuery, userQuery) + err := query(db, userSession) + return userSession, err +} + +func UserSessionsByAgentID(db *gorm.DB, table, agentID string) ([]*model.UserSessionView, error) { + userSessions := make([]*model.UserSessionView, 0) + userAgentQuery := &usr_model.UserSessionSearchQuery{ + Key: usr_model.USERSESSIONSEARCHKEY_USER_AGENT_ID, + Method: global_model.SEARCHMETHOD_EQUALS, + Value: agentID, + } + query := view.PrepareSearchQuery(table, model.UserSessionSearchRequest{ + Queries: []*usr_model.UserSessionSearchQuery{userAgentQuery}, + }) + _, err := query(db, userSessions) + return userSessions, err +} + +func PutUserSession(db *gorm.DB, table string, session *model.UserSessionView) error { + save := view.PrepareSave(table) + return save(db, session) +} + +func DeleteUserSession(db *gorm.DB, table, sessionID string) error { + delete := view.PrepareDeleteByKey(table, model.UserSessionSearchKey(usr_model.USERSESSIONSEARCHKEY_USER_ID), sessionID) + return delete(db) +} diff --git a/internal/view/db_mock_test.go b/internal/view/db_mock_test.go index 0c9440c986..438f2554a8 100644 --- a/internal/view/db_mock_test.go +++ b/internal/view/db_mock_test.go @@ -1,19 +1,31 @@ package view import ( + "database/sql/driver" "fmt" - "github.com/DATA-DOG/go-sqlmock" - "github.com/caos/zitadel/internal/model" - "github.com/jinzhu/gorm" + "strconv" "testing" + + "github.com/DATA-DOG/go-sqlmock" + "github.com/jinzhu/gorm" + + "github.com/caos/zitadel/internal/model" ) var ( - expectedGetByID = `SELECT \* FROM "%s" WHERE \(%s = \$1\) LIMIT 1` - expectedGetByQuery = `SELECT \* FROM "%s" WHERE \(LOWER\(%s\) %s LOWER\(\$1\)\) LIMIT 1` - expectedGetByQueryCaseSensitive = `SELECT \* FROM "%s" WHERE \(%s %s \$1\) LIMIT 1` - expectedSave = `UPDATE "%s" SET "test" = \$1 WHERE "%s"."%s" = \$2` - expectedRemove = `DELETE FROM "%s" WHERE \(%s = \$1\)` + expectedGetByID = `SELECT \* FROM "%s" WHERE \(%s = \$1\) LIMIT 1` + expectedGetByQuery = `SELECT \* FROM "%s" WHERE \(LOWER\(%s\) %s LOWER\(\$1\)\) LIMIT 1` + expectedGetByQueryCaseSensitive = `SELECT \* FROM "%s" WHERE \(%s %s \$1\) LIMIT 1` + expectedSave = `UPDATE "%s" SET "test" = \$1 WHERE "%s"."%s" = \$2` + expectedRemove = `DELETE FROM "%s" WHERE \(%s = \$1\)` + expectedRemoveByKeys = func(i int, table string) string { + sql := fmt.Sprintf(`DELETE FROM "%s"`, table) + sql += ` WHERE \(%s = \$1\)` + for j := 1; j < i; j++ { + sql = sql + ` AND \(%s = \$` + strconv.Itoa(j+1) + `\)` + } + return sql + } expectedRemoveByObject = `DELETE FROM "%s" WHERE "%s"."%s" = \$1` expectedRemoveByObjectMultiplePK = `DELETE FROM "%s" WHERE "%s"."%s" = \$1 AND "%s"."%s" = \$2` expectedSearch = `SELECT \* FROM "%s" OFFSET 0` @@ -235,6 +247,21 @@ func (db *dbMock) expectRemove(table, key, value string) *dbMock { return db } +func (db *dbMock) expectRemoveKeys(table string, keys ...Key) *dbMock { + keynames := make([]interface{}, len(keys)) + keyvalues := make([]driver.Value, len(keys)) + for i, key := range keys { + keynames[i] = key.Key.ToColumnName() + keyvalues[i] = key.Value + } + query := fmt.Sprintf(expectedRemoveByKeys(len(keys), table), keynames...) + db.mock.ExpectExec(query). + WithArgs(keyvalues...). + WillReturnResult(sqlmock.NewResult(1, 1)) + + return db +} + func (db *dbMock) expectRemoveByObject(table string, object Test) *dbMock { query := fmt.Sprintf(expectedRemoveByObject, table, table, "primary_id") db.mock.ExpectExec(query). diff --git a/internal/view/requests.go b/internal/view/requests.go index deffeee8cb..22276138a4 100644 --- a/internal/view/requests.go +++ b/internal/view/requests.go @@ -3,9 +3,11 @@ package view import ( "errors" "fmt" + "github.com/caos/logging" - caos_errs "github.com/caos/zitadel/internal/errors" "github.com/jinzhu/gorm" + + caos_errs "github.com/caos/zitadel/internal/errors" ) func PrepareGetByKey(table string, key ColumnKey, id string) func(db *gorm.DB, res interface{}) error { @@ -71,6 +73,27 @@ func PrepareDeleteByKey(table string, key ColumnKey, id string) func(db *gorm.DB } } +type Key struct { + Key ColumnKey + Value string +} + +func PrepareDeleteByKeys(table string, keys ...Key) func(db *gorm.DB) error { + return func(db *gorm.DB) error { + for _, key := range keys { + db = db.Table(table). + Where(fmt.Sprintf("%s = ?", key.Key.ToColumnName()), key.Value) + } + err := db. + Delete(nil). + Error + if err != nil { + return caos_errs.ThrowInternal(err, "VIEW-die73", "could not delete object") + } + return nil + } +} + func PrepareDeleteByObject(table string, object interface{}) func(db *gorm.DB) error { return func(db *gorm.DB) error { err := db.Table(table). diff --git a/internal/view/requests_test.go b/internal/view/requests_test.go index 1d8c3d01ba..c5c3007ff4 100644 --- a/internal/view/requests_test.go +++ b/internal/view/requests_test.go @@ -391,6 +391,97 @@ func TestPrepareDelete(t *testing.T) { } } +func TestPrepareDeleteByKeys(t *testing.T) { + type args struct { + table string + keys []Key + } + type res struct { + result Test + wantErr bool + errFunc func(err error) bool + } + tests := []struct { + name string + db *dbMock + args args + res res + }{ + { + "delete single key", + mockDB(t). + expectBegin(nil). + expectRemoveKeys("TESTTABLE", Key{Key: TestSearchKey_ID, Value: "VALUE"}). + expectCommit(nil), + args{ + table: "TESTTABLE", + keys: []Key{ + {Key: TestSearchKey_ID, Value: "VALUE"}, + }, + }, + res{ + result: Test{ID: "VALUE"}, + wantErr: false, + }, + }, + { + "delete multiple keys", + mockDB(t). + expectBegin(nil). + expectRemoveKeys("TESTTABLE", Key{Key: TestSearchKey_ID, Value: "VALUE"}, Key{Key: TestSearchKey_TEST, Value: "VALUE2"}). + expectCommit(nil), + args{ + table: "TESTTABLE", + keys: []Key{ + {Key: TestSearchKey_ID, Value: "VALUE"}, + {Key: TestSearchKey_TEST, Value: "VALUE2"}, + }, + }, + res{ + result: Test{ID: "VALUE"}, + wantErr: false, + }, + }, + { + "db error", + mockDB(t). + expectBegin(nil). + expectRemoveErr("TESTTABLE", "id", "VALUE", gorm.ErrUnaddressable). + expectCommit(nil), + args{ + table: "TESTTABLE", + keys: []Key{ + {Key: TestSearchKey_ID, Value: "VALUE"}, + }, + }, + res{ + result: Test{ID: "VALUE"}, + wantErr: true, + errFunc: caos_errs.IsInternal, + }, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + getDelete := PrepareDeleteByKeys(tt.args.table, tt.args.keys...) + err := getDelete(tt.db.db) + + if !tt.res.wantErr && err != nil { + t.Errorf("got wrong err should be nil: %v ", err) + } + + if tt.res.wantErr && !tt.res.errFunc(err) { + t.Errorf("got wrong err: %v ", err) + } + if err := tt.db.mock.ExpectationsWereMet(); !tt.res.wantErr && err != nil { + t.Errorf("there were unfulfilled expectations: %s", err) + } + + tt.db.close() + }) + } +} + func TestPrepareDeleteByObject(t *testing.T) { type args struct { table string diff --git a/migrations/cockroach/V1.5__auth.sql b/migrations/cockroach/V1.5__auth.sql new file mode 100644 index 0000000000..d483a62898 --- /dev/null +++ b/migrations/cockroach/V1.5__auth.sql @@ -0,0 +1,121 @@ +BEGIN; + +CREATE DATABASE auth; + + +COMMIT; + +BEGIN; + +CREATE USER auth; + +GRANT SELECT, INSERT, UPDATE, DELETE ON DATABASE auth TO auth; +GRANT SELECT, INSERT, UPDATE ON DATABASE eventstore TO auth; +GRANT SELECT, INSERT, UPDATE ON TABLE eventstore.* TO auth; + +COMMIT; + +BEGIN; + +CREATE TABLE auth.locks ( + locker_id TEXT, + locked_until TIMESTAMPTZ, + object_type TEXT, + + PRIMARY KEY (object_type) +); + +CREATE TABLE auth.current_sequences ( + view_name TEXT, + + current_sequence BIGINT, + + PRIMARY KEY (view_name) +); + +CREATE TABLE auth.failed_event ( + view_name TEXT, + failed_sequence BIGINT, + failure_count SMALLINT, + err_msg TEXT, + + PRIMARY KEY (view_name, failed_sequence) +); + +CREATE TABLE auth.auth_requests ( + id TEXT, + request JSONB, + + PRIMARY KEY (id) +); + +CREATE TABLE auth.users ( + id TEXT, + + creation_date TIMESTAMPTZ, + change_date TIMESTAMPTZ, + + resource_owner TEXT, + user_state SMALLINT, + password_set BOOLEAN, + password_change_required BOOLEAN, + password_change TIMESTAMPTZ, + last_login TIMESTAMPTZ, + user_name TEXT, + first_name TEXT, + last_name TEXT, + nick_name TEXT, + display_name TEXT, + preferred_language TEXT, + gender SMALLINT, + email TEXT, + is_email_verified BOOLEAN, + phone TEXT, + is_phone_verified BOOLEAN, + country TEXT, + locality TEXT, + postal_code TEXT, + region TEXT, + street_address TEXT, + otp_state SMALLINT, + mfa_max_set_up SMALLINT, + mfa_init_skipped TIMESTAMPTZ, + sequence BIGINT, + + PRIMARY KEY (id) +); + +CREATE TABLE auth.user_sessions ( + creation_date TIMESTAMPTZ, + change_date TIMESTAMPTZ, + + resource_owner TEXT, + state SMALLINT, + user_agent_id TEXT, + user_id TEXT, + user_name TEXT, + password_verification TIMESTAMPTZ, + mfa_software_verification TIMESTAMPTZ, + mfa_hardware_verification TIMESTAMPTZ, + sequence BIGINT, + + PRIMARY KEY (user_agent_id, user_id) +); + +CREATE TABLE auth.tokens ( + id TEXT, + + creation_date TIMESTAMPTZ, + change_date TIMESTAMPTZ, + + resource_owner TEXT, + application_id TEXT, + user_agent_id TEXT, + user_id TEXT, + expiration TIMESTAMPTZ, + sequence BIGINT, + + PRIMARY KEY (id) +); + +COMMIT; \ No newline at end of file diff --git a/pkg/auth/api/api.go b/pkg/auth/api/api.go index e59bdd1bec..737af9eb8b 100644 --- a/pkg/auth/api/api.go +++ b/pkg/auth/api/api.go @@ -3,8 +3,10 @@ package api import ( "context" + auth_util "github.com/caos/zitadel/internal/api/auth" grpc_util "github.com/caos/zitadel/internal/api/grpc" "github.com/caos/zitadel/internal/api/grpc/server" + "github.com/caos/zitadel/internal/auth/repository" "github.com/caos/zitadel/pkg/auth/api/grpc" ) @@ -12,8 +14,8 @@ type Config struct { GRPC grpc_util.Config } -func Start(ctx context.Context, conf Config) { - grpcServer := grpc.StartServer(conf.GRPC.ToServerConfig()) +func Start(ctx context.Context, conf Config, authZ auth_util.Config, repo repository.Repository) { + grpcServer := grpc.StartServer(conf.GRPC.ToServerConfig(), authZ, repo) grpcGateway := grpc.StartGateway(conf.GRPC.ToGatewayConfig()) server.StartServer(ctx, grpcServer) diff --git a/pkg/auth/api/grpc/auth.pb.authoptions.go b/pkg/auth/api/grpc/auth.pb.authoptions.go index c84b7dc329..ee4700c496 100644 --- a/pkg/auth/api/grpc/auth.pb.authoptions.go +++ b/pkg/auth/api/grpc/auth.pb.authoptions.go @@ -85,11 +85,6 @@ var AuthService_AuthMethods = utils_auth.MethodMapping{ CheckParam: "", }, - "/zitadel.auth.api.v1.AuthService/SetMyPassword": utils_auth.Option{ - Permission: "authenticated", - CheckParam: "", - }, - "/zitadel.auth.api.v1.AuthService/ChangeMyPassword": utils_auth.Option{ Permission: "authenticated", CheckParam: "", diff --git a/pkg/auth/api/grpc/auth.pb.go b/pkg/auth/api/grpc/auth.pb.go index 3f3c97b847..2eae69dd91 100644 --- a/pkg/auth/api/grpc/auth.pb.go +++ b/pkg/auth/api/grpc/auth.pb.go @@ -620,18 +620,20 @@ func (m *User) GetSequence() uint64 { } type UserProfile struct { - Id string `protobuf:"bytes,1,opt,name=id,proto3" json:"id,omitempty"` - UserName string `protobuf:"bytes,2,opt,name=user_name,json=userName,proto3" json:"user_name,omitempty"` - FirstName string `protobuf:"bytes,3,opt,name=first_name,json=firstName,proto3" json:"first_name,omitempty"` - LastName string `protobuf:"bytes,4,opt,name=last_name,json=lastName,proto3" json:"last_name,omitempty"` - NickName string `protobuf:"bytes,5,opt,name=nick_name,json=nickName,proto3" json:"nick_name,omitempty"` - DisplayName string `protobuf:"bytes,6,opt,name=display_name,json=displayName,proto3" json:"display_name,omitempty"` - PreferredLanguage string `protobuf:"bytes,7,opt,name=preferred_language,json=preferredLanguage,proto3" json:"preferred_language,omitempty"` - Gender Gender `protobuf:"varint,8,opt,name=gender,proto3,enum=zitadel.auth.api.v1.Gender" json:"gender,omitempty"` - Sequence uint64 `protobuf:"varint,26,opt,name=sequence,proto3" json:"sequence,omitempty"` - XXX_NoUnkeyedLiteral struct{} `json:"-"` - XXX_unrecognized []byte `json:"-"` - XXX_sizecache int32 `json:"-"` + Id string `protobuf:"bytes,1,opt,name=id,proto3" json:"id,omitempty"` + UserName string `protobuf:"bytes,2,opt,name=user_name,json=userName,proto3" json:"user_name,omitempty"` + FirstName string `protobuf:"bytes,3,opt,name=first_name,json=firstName,proto3" json:"first_name,omitempty"` + LastName string `protobuf:"bytes,4,opt,name=last_name,json=lastName,proto3" json:"last_name,omitempty"` + NickName string `protobuf:"bytes,5,opt,name=nick_name,json=nickName,proto3" json:"nick_name,omitempty"` + DisplayName string `protobuf:"bytes,6,opt,name=display_name,json=displayName,proto3" json:"display_name,omitempty"` + PreferredLanguage string `protobuf:"bytes,7,opt,name=preferred_language,json=preferredLanguage,proto3" json:"preferred_language,omitempty"` + Gender Gender `protobuf:"varint,8,opt,name=gender,proto3,enum=zitadel.auth.api.v1.Gender" json:"gender,omitempty"` + Sequence uint64 `protobuf:"varint,9,opt,name=sequence,proto3" json:"sequence,omitempty"` + CreationDate *timestamp.Timestamp `protobuf:"bytes,10,opt,name=creation_date,json=creationDate,proto3" json:"creation_date,omitempty"` + ChangeDate *timestamp.Timestamp `protobuf:"bytes,11,opt,name=change_date,json=changeDate,proto3" json:"change_date,omitempty"` + XXX_NoUnkeyedLiteral struct{} `json:"-"` + XXX_unrecognized []byte `json:"-"` + XXX_sizecache int32 `json:"-"` } func (m *UserProfile) Reset() { *m = UserProfile{} } @@ -722,6 +724,20 @@ func (m *UserProfile) GetSequence() uint64 { return 0 } +func (m *UserProfile) GetCreationDate() *timestamp.Timestamp { + if m != nil { + return m.CreationDate + } + return nil +} + +func (m *UserProfile) GetChangeDate() *timestamp.Timestamp { + if m != nil { + return m.ChangeDate + } + return nil +} + type UpdateUserProfileRequest struct { FirstName string `protobuf:"bytes,1,opt,name=first_name,json=firstName,proto3" json:"first_name,omitempty"` LastName string `protobuf:"bytes,2,opt,name=last_name,json=lastName,proto3" json:"last_name,omitempty"` @@ -802,13 +818,15 @@ func (m *UpdateUserProfileRequest) GetGender() Gender { } type UserEmail struct { - Id string `protobuf:"bytes,1,opt,name=id,proto3" json:"id,omitempty"` - Email string `protobuf:"bytes,2,opt,name=email,proto3" json:"email,omitempty"` - IsEmailVerified bool `protobuf:"varint,3,opt,name=isEmailVerified,proto3" json:"isEmailVerified,omitempty"` - Sequence uint64 `protobuf:"varint,4,opt,name=sequence,proto3" json:"sequence,omitempty"` - XXX_NoUnkeyedLiteral struct{} `json:"-"` - XXX_unrecognized []byte `json:"-"` - XXX_sizecache int32 `json:"-"` + Id string `protobuf:"bytes,1,opt,name=id,proto3" json:"id,omitempty"` + Email string `protobuf:"bytes,2,opt,name=email,proto3" json:"email,omitempty"` + IsEmailVerified bool `protobuf:"varint,3,opt,name=isEmailVerified,proto3" json:"isEmailVerified,omitempty"` + Sequence uint64 `protobuf:"varint,4,opt,name=sequence,proto3" json:"sequence,omitempty"` + CreationDate *timestamp.Timestamp `protobuf:"bytes,5,opt,name=creation_date,json=creationDate,proto3" json:"creation_date,omitempty"` + ChangeDate *timestamp.Timestamp `protobuf:"bytes,6,opt,name=change_date,json=changeDate,proto3" json:"change_date,omitempty"` + XXX_NoUnkeyedLiteral struct{} `json:"-"` + XXX_unrecognized []byte `json:"-"` + XXX_sizecache int32 `json:"-"` } func (m *UserEmail) Reset() { *m = UserEmail{} } @@ -864,6 +882,20 @@ func (m *UserEmail) GetSequence() uint64 { return 0 } +func (m *UserEmail) GetCreationDate() *timestamp.Timestamp { + if m != nil { + return m.CreationDate + } + return nil +} + +func (m *UserEmail) GetChangeDate() *timestamp.Timestamp { + if m != nil { + return m.ChangeDate + } + return nil +} + type VerifyMyUserEmailRequest struct { Code string `protobuf:"bytes,1,opt,name=code,proto3" json:"code,omitempty"` XXX_NoUnkeyedLiteral struct{} `json:"-"` @@ -990,13 +1022,15 @@ func (m *UpdateUserEmailRequest) GetEmail() string { } type UserPhone struct { - Id string `protobuf:"bytes,1,opt,name=id,proto3" json:"id,omitempty"` - Phone string `protobuf:"bytes,2,opt,name=phone,proto3" json:"phone,omitempty"` - IsPhoneVerified bool `protobuf:"varint,3,opt,name=is_phone_verified,json=isPhoneVerified,proto3" json:"is_phone_verified,omitempty"` - Sequence uint64 `protobuf:"varint,4,opt,name=sequence,proto3" json:"sequence,omitempty"` - XXX_NoUnkeyedLiteral struct{} `json:"-"` - XXX_unrecognized []byte `json:"-"` - XXX_sizecache int32 `json:"-"` + Id string `protobuf:"bytes,1,opt,name=id,proto3" json:"id,omitempty"` + Phone string `protobuf:"bytes,2,opt,name=phone,proto3" json:"phone,omitempty"` + IsPhoneVerified bool `protobuf:"varint,3,opt,name=is_phone_verified,json=isPhoneVerified,proto3" json:"is_phone_verified,omitempty"` + Sequence uint64 `protobuf:"varint,4,opt,name=sequence,proto3" json:"sequence,omitempty"` + CreationDate *timestamp.Timestamp `protobuf:"bytes,5,opt,name=creation_date,json=creationDate,proto3" json:"creation_date,omitempty"` + ChangeDate *timestamp.Timestamp `protobuf:"bytes,6,opt,name=change_date,json=changeDate,proto3" json:"change_date,omitempty"` + XXX_NoUnkeyedLiteral struct{} `json:"-"` + XXX_unrecognized []byte `json:"-"` + XXX_sizecache int32 `json:"-"` } func (m *UserPhone) Reset() { *m = UserPhone{} } @@ -1052,6 +1086,20 @@ func (m *UserPhone) GetSequence() uint64 { return 0 } +func (m *UserPhone) GetCreationDate() *timestamp.Timestamp { + if m != nil { + return m.CreationDate + } + return nil +} + +func (m *UserPhone) GetChangeDate() *timestamp.Timestamp { + if m != nil { + return m.ChangeDate + } + return nil +} + type UpdateUserPhoneRequest struct { Phone string `protobuf:"bytes,1,opt,name=phone,proto3" json:"phone,omitempty"` XXX_NoUnkeyedLiteral struct{} `json:"-"` @@ -1131,16 +1179,18 @@ func (m *VerifyUserPhoneRequest) GetCode() string { } type UserAddress struct { - Id string `protobuf:"bytes,1,opt,name=id,proto3" json:"id,omitempty"` - Country string `protobuf:"bytes,2,opt,name=country,proto3" json:"country,omitempty"` - Locality string `protobuf:"bytes,3,opt,name=locality,proto3" json:"locality,omitempty"` - PostalCode string `protobuf:"bytes,4,opt,name=postal_code,json=postalCode,proto3" json:"postal_code,omitempty"` - Region string `protobuf:"bytes,5,opt,name=region,proto3" json:"region,omitempty"` - StreetAddress string `protobuf:"bytes,6,opt,name=street_address,json=streetAddress,proto3" json:"street_address,omitempty"` - Sequence uint64 `protobuf:"varint,7,opt,name=sequence,proto3" json:"sequence,omitempty"` - XXX_NoUnkeyedLiteral struct{} `json:"-"` - XXX_unrecognized []byte `json:"-"` - XXX_sizecache int32 `json:"-"` + Id string `protobuf:"bytes,1,opt,name=id,proto3" json:"id,omitempty"` + Country string `protobuf:"bytes,2,opt,name=country,proto3" json:"country,omitempty"` + Locality string `protobuf:"bytes,3,opt,name=locality,proto3" json:"locality,omitempty"` + PostalCode string `protobuf:"bytes,4,opt,name=postal_code,json=postalCode,proto3" json:"postal_code,omitempty"` + Region string `protobuf:"bytes,5,opt,name=region,proto3" json:"region,omitempty"` + StreetAddress string `protobuf:"bytes,6,opt,name=street_address,json=streetAddress,proto3" json:"street_address,omitempty"` + Sequence uint64 `protobuf:"varint,7,opt,name=sequence,proto3" json:"sequence,omitempty"` + CreationDate *timestamp.Timestamp `protobuf:"bytes,8,opt,name=creation_date,json=creationDate,proto3" json:"creation_date,omitempty"` + ChangeDate *timestamp.Timestamp `protobuf:"bytes,9,opt,name=change_date,json=changeDate,proto3" json:"change_date,omitempty"` + XXX_NoUnkeyedLiteral struct{} `json:"-"` + XXX_unrecognized []byte `json:"-"` + XXX_sizecache int32 `json:"-"` } func (m *UserAddress) Reset() { *m = UserAddress{} } @@ -1217,6 +1267,20 @@ func (m *UserAddress) GetSequence() uint64 { return 0 } +func (m *UserAddress) GetCreationDate() *timestamp.Timestamp { + if m != nil { + return m.CreationDate + } + return nil +} + +func (m *UserAddress) GetChangeDate() *timestamp.Timestamp { + if m != nil { + return m.ChangeDate + } + return nil +} + type UpdateUserAddressRequest struct { Country string `protobuf:"bytes,1,opt,name=country,proto3" json:"country,omitempty"` Locality string `protobuf:"bytes,2,opt,name=locality,proto3" json:"locality,omitempty"` @@ -1996,189 +2060,190 @@ func init() { func init() { proto.RegisterFile("auth.proto", fileDescriptor_8bbd6f3875b0e874) } var fileDescriptor_8bbd6f3875b0e874 = []byte{ - // 2761 bytes of a gzipped FileDescriptorProto - 0x1f, 0x8b, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0xff, 0xb4, 0x5a, 0x4d, 0x6f, 0xdb, 0xc8, - 0x19, 0x0e, 0x25, 0x59, 0x96, 0x5f, 0xf9, 0x83, 0x1e, 0x27, 0x36, 0x2d, 0x3b, 0x89, 0xc2, 0x6c, - 0xb0, 0x8e, 0x36, 0xb1, 0x36, 0xde, 0x2d, 0xba, 0xc9, 0x1e, 0x0a, 0xc5, 0xa2, 0x13, 0x35, 0xd6, - 0xc7, 0x52, 0x4a, 0xda, 0x2c, 0x50, 0xa8, 0x8c, 0x38, 0x92, 0xb9, 0x2b, 0x89, 0x0c, 0x49, 0x39, - 0xd0, 0x5e, 0x5a, 0xec, 0x69, 0xdb, 0xcb, 0xa2, 0xdb, 0x43, 0x8f, 0x3d, 0xf4, 0x0f, 0xb4, 0x05, - 0xda, 0x02, 0xfd, 0x05, 0xbd, 0xf5, 0xd0, 0xfe, 0x82, 0xa2, 0xbf, 0x22, 0x87, 0xa2, 0x98, 0x0f, - 0x4a, 0xfc, 0x10, 0x25, 0x05, 0x45, 0x4f, 0xe1, 0xbc, 0x5f, 0xf3, 0xbc, 0xcf, 0xbc, 0xf3, 0x6a, - 0x66, 0x1c, 0x00, 0x6d, 0xe4, 0x5e, 0x1c, 0x5b, 0xb6, 0xe9, 0x9a, 0x68, 0xe7, 0x2b, 0xc3, 0xd5, - 0x74, 0xdc, 0x3f, 0xa6, 0x32, 0xcd, 0x32, 0x8e, 0x2f, 0x1f, 0xe4, 0x0e, 0x7b, 0xa6, 0xd9, 0xeb, - 0xe3, 0xa2, 0x66, 0x19, 0x45, 0x6d, 0x38, 0x34, 0x5d, 0xcd, 0x35, 0xcc, 0xa1, 0xc3, 0x5c, 0x72, - 0x07, 0x5c, 0x4b, 0x47, 0xaf, 0x46, 0xdd, 0x22, 0x1e, 0x58, 0xee, 0x98, 0x2b, 0x0f, 0xc3, 0x4a, - 0xc7, 0xb5, 0x47, 0x1d, 0x97, 0x6b, 0x6f, 0x86, 0xb5, 0xae, 0x31, 0xc0, 0x8e, 0xab, 0x0d, 0x2c, - 0x6e, 0xb0, 0x77, 0xa9, 0xf5, 0x0d, 0x5d, 0x73, 0x71, 0xd1, 0xfb, 0xe0, 0x8a, 0x7b, 0xf4, 0x9f, - 0xce, 0xfd, 0x1e, 0x1e, 0xde, 0x77, 0xde, 0x68, 0xbd, 0x1e, 0xb6, 0x8b, 0xa6, 0x45, 0x61, 0xcd, - 0x80, 0x28, 0x91, 0x6c, 0x98, 0xda, 0xb3, 0x62, 0x1a, 0xf9, 0x27, 0x20, 0x3e, 0x77, 0xb0, 0xdd, - 0xc4, 0x8e, 0x63, 0x98, 0xc3, 0x17, 0x06, 0x7e, 0xe3, 0xa0, 0x0a, 0x6c, 0x8c, 0x1c, 0x6c, 0xb7, - 0x1d, 0x26, 0x74, 0x24, 0x21, 0x9f, 0x3c, 0xca, 0x9e, 0xbc, 0x77, 0x3c, 0x83, 0x9b, 0xe3, 0x90, - 0xb7, 0xba, 0x3e, 0x9a, 0x0a, 0x1c, 0xf9, 0x9f, 0x02, 0x6c, 0x85, 0x2c, 0xd0, 0x26, 0x24, 0x0c, - 0x5d, 0x12, 0xf2, 0xc2, 0xd1, 0x9a, 0x9a, 0x30, 0x74, 0xb4, 0x0f, 0x19, 0xad, 0x87, 0x87, 0x6e, - 0xdb, 0xd0, 0xa5, 0x04, 0x95, 0xae, 0xd2, 0x71, 0x45, 0x47, 0x65, 0xb6, 0x36, 0x6d, 0xc7, 0xd5, - 0x5c, 0x2c, 0x25, 0xf3, 0xc2, 0xd1, 0xe6, 0xc9, 0x9d, 0x45, 0x30, 0x9a, 0xc4, 0x58, 0x5d, 0x23, - 0x5a, 0xfa, 0x89, 0xf6, 0x60, 0x95, 0xe6, 0x63, 0xe8, 0x52, 0x8a, 0xc6, 0x4f, 0x93, 0x61, 0x45, - 0x47, 0x07, 0xb0, 0x46, 0x15, 0x43, 0x6d, 0x80, 0xa5, 0x15, 0xaa, 0xca, 0x10, 0x41, 0x4d, 0x1b, - 0x60, 0x94, 0x83, 0x8c, 0x83, 0x5f, 0x8f, 0xf0, 0xb0, 0x83, 0xa5, 0x74, 0x5e, 0x38, 0x4a, 0xa9, - 0x93, 0xb1, 0xfc, 0x9f, 0x55, 0x48, 0x91, 0x19, 0x23, 0xb9, 0x7c, 0x0c, 0x2b, 0x0c, 0x6b, 0x82, - 0x62, 0xbd, 0x11, 0x8f, 0x95, 0x82, 0x64, 0xc6, 0xe8, 0x07, 0xb0, 0xd1, 0xb1, 0x31, 0x5d, 0xb1, - 0xb6, 0xee, 0x65, 0x9a, 0x3d, 0xc9, 0x1d, 0xb3, 0xf2, 0x38, 0xf6, 0xca, 0xe3, 0xb8, 0xe5, 0x95, - 0x87, 0xba, 0xee, 0x39, 0x94, 0x49, 0x80, 0x53, 0xd8, 0xd2, 0x3a, 0xae, 0x71, 0xe9, 0x0b, 0x91, - 0x5a, 0x18, 0x62, 0x73, 0xea, 0x42, 0x83, 0x7c, 0x0a, 0xd9, 0xce, 0x85, 0x36, 0xec, 0x61, 0x16, - 0x60, 0x65, 0x61, 0x00, 0x60, 0xe6, 0xd4, 0xf9, 0x21, 0x40, 0x5f, 0x73, 0xdc, 0x76, 0xdf, 0xec, - 0x19, 0x43, 0xca, 0xd7, 0x7c, 0xdf, 0x35, 0x62, 0x7d, 0x4e, 0x8c, 0x91, 0x02, 0xa2, 0xa5, 0x39, - 0xce, 0x1b, 0xd3, 0xd6, 0xdb, 0x2c, 0xa2, 0x2e, 0xad, 0x2e, 0x0c, 0xb0, 0xe5, 0xf9, 0x9c, 0x32, - 0x97, 0xe0, 0x62, 0x66, 0x42, 0x8b, 0x79, 0x1d, 0xa0, 0x6b, 0xd8, 0x8e, 0xcb, 0xb4, 0x6b, 0x54, - 0xbb, 0x46, 0x25, 0x54, 0x7d, 0x00, 0x14, 0x0f, 0xd3, 0x02, 0xf3, 0x25, 0x02, 0x4f, 0x39, 0x34, - 0x3a, 0x5f, 0x32, 0x65, 0x96, 0x29, 0x89, 0x80, 0x2a, 0x6f, 0xc1, 0xba, 0x6e, 0x38, 0x56, 0x5f, - 0x1b, 0x33, 0xfd, 0x3a, 0xd5, 0x67, 0xb9, 0x8c, 0x9a, 0xdc, 0x07, 0x64, 0xd9, 0xb8, 0x8b, 0x6d, - 0x1b, 0xeb, 0xed, 0xbe, 0x36, 0xec, 0x8d, 0xb4, 0x1e, 0x96, 0x36, 0xa8, 0xe1, 0xf6, 0x44, 0x73, - 0xce, 0x15, 0xe8, 0x23, 0x48, 0xf7, 0xf0, 0x50, 0xc7, 0xb6, 0xb4, 0x49, 0x6b, 0xe8, 0x60, 0x66, - 0x0d, 0x3d, 0xa1, 0x26, 0x2a, 0x37, 0x45, 0x57, 0x61, 0x05, 0x0f, 0x34, 0xa3, 0x2f, 0x6d, 0xd1, - 0xb0, 0x6c, 0x80, 0x0a, 0xb0, 0x6d, 0x38, 0x6d, 0xfa, 0xdd, 0xbe, 0xc4, 0xb6, 0xd1, 0x35, 0xb0, - 0x2e, 0x89, 0x79, 0xe1, 0x28, 0xa3, 0x6e, 0x19, 0x8e, 0x42, 0xe4, 0x2f, 0xb8, 0x98, 0x44, 0xb0, - 0x2e, 0xcc, 0x21, 0x96, 0xb6, 0x59, 0x04, 0x3a, 0xe0, 0x11, 0xe8, 0xf7, 0x34, 0x02, 0xf2, 0x22, - 0x34, 0x88, 0x7c, 0x12, 0x41, 0x82, 0xd5, 0x8e, 0x39, 0x1a, 0xba, 0xf6, 0x58, 0xda, 0x61, 0xdb, - 0x98, 0x0f, 0xc9, 0x56, 0xea, 0x9b, 0x1d, 0xad, 0x6f, 0xb8, 0x63, 0xe9, 0x2a, 0x67, 0x97, 0x8f, - 0xd1, 0x4d, 0xc8, 0x5a, 0xa6, 0xe3, 0x6a, 0xfd, 0x76, 0xc7, 0xd4, 0xb1, 0x74, 0x8d, 0xaa, 0x81, - 0x89, 0x4e, 0x4d, 0x1d, 0xa3, 0x5d, 0x48, 0xdb, 0xb8, 0x67, 0x98, 0x43, 0x69, 0x97, 0x6d, 0x5e, - 0x36, 0x42, 0x77, 0x60, 0xd3, 0x71, 0x6d, 0x8c, 0xdd, 0xb6, 0xa6, 0xeb, 0x36, 0x76, 0x1c, 0x69, - 0x8f, 0xea, 0x37, 0x98, 0xb4, 0xc4, 0x84, 0xe8, 0x13, 0x90, 0x42, 0xd5, 0xd5, 0xb6, 0xf1, 0xeb, - 0x91, 0x61, 0x63, 0x5d, 0x92, 0x68, 0x22, 0xbb, 0xc1, 0x4a, 0x52, 0xb9, 0x36, 0xd0, 0x00, 0xf6, - 0x43, 0x0d, 0xe0, 0xcf, 0x09, 0xc8, 0x92, 0x6d, 0xdc, 0xb0, 0xcd, 0xae, 0xd1, 0xc7, 0x91, 0x3e, - 0x10, 0x28, 0xc6, 0xc4, 0xdc, 0x62, 0x4c, 0xce, 0x2d, 0xc6, 0xd4, 0xbc, 0x62, 0x5c, 0x59, 0x50, - 0x8c, 0xe9, 0x65, 0x8b, 0x71, 0x75, 0x71, 0x31, 0x66, 0x96, 0x2f, 0x46, 0x3f, 0x71, 0xb9, 0x28, - 0x71, 0xd2, 0x73, 0x8b, 0x34, 0x18, 0x1f, 0x7d, 0x84, 0x72, 0xec, 0xb8, 0xe8, 0x6e, 0x80, 0x18, - 0xca, 0xe6, 0x63, 0x78, 0xfb, 0x78, 0xd5, 0x5e, 0x11, 0x05, 0xe9, 0x6f, 0x82, 0x9f, 0xa4, 0xf7, - 0xfd, 0x24, 0x25, 0x22, 0x96, 0x53, 0xc2, 0xde, 0xf7, 0x13, 0x96, 0x8c, 0x1a, 0x4e, 0xc8, 0xbb, - 0x1f, 0x22, 0x2f, 0x15, 0xb1, 0x0d, 0x10, 0xf9, 0x70, 0x26, 0x91, 0x2b, 0x11, 0xa7, 0xb9, 0xa4, - 0xa6, 0x97, 0x26, 0x55, 0x7e, 0x03, 0x6b, 0x84, 0x31, 0xba, 0x69, 0x23, 0xe5, 0x36, 0xd9, 0xfe, - 0x09, 0xff, 0xf6, 0x3f, 0x82, 0xf0, 0x2e, 0xa7, 0x04, 0xcc, 0xd8, 0xfc, 0xfe, 0x15, 0x4b, 0x85, - 0x56, 0xec, 0x11, 0x48, 0xd4, 0x6e, 0x5c, 0x1d, 0x4f, 0x00, 0x78, 0x0b, 0x76, 0x03, 0x52, 0x74, - 0xd7, 0x46, 0x97, 0x8a, 0xca, 0xe5, 0xa7, 0xb0, 0xcb, 0x7c, 0x23, 0x9e, 0xe1, 0x0c, 0xbc, 0x48, - 0x89, 0x98, 0x48, 0x8f, 0x60, 0x77, 0x5a, 0x36, 0x81, 0x48, 0x79, 0x2f, 0xf7, 0x28, 0x08, 0xa6, - 0x90, 0xc7, 0x8c, 0x3a, 0xda, 0xad, 0x66, 0x51, 0xc7, 0xfa, 0x5e, 0x62, 0x61, 0xdf, 0x4b, 0xce, - 0xee, 0x7b, 0xf3, 0xc8, 0x7b, 0xe8, 0x87, 0x4d, 0xdd, 0x3c, 0xd8, 0x37, 0xbd, 0x79, 0x19, 0xec, - 0xb5, 0xb7, 0x8f, 0xd3, 0x76, 0x4a, 0x14, 0xa4, 0xab, 0x1c, 0x82, 0xfc, 0x89, 0x9f, 0xbb, 0x80, - 0xeb, 0x22, 0xd6, 0xff, 0x2e, 0xb0, 0xe6, 0xe4, 0xb5, 0xc0, 0x70, 0xca, 0xbe, 0x46, 0x9d, 0x88, - 0x6f, 0xd4, 0xc9, 0xf9, 0x8d, 0x3a, 0x35, 0xa7, 0x51, 0xaf, 0x2c, 0x68, 0xd4, 0xe9, 0x59, 0x8d, - 0xda, 0x4f, 0xe3, 0x6a, 0x88, 0xc6, 0x7f, 0x09, 0xfe, 0xae, 0xc1, 0x3d, 0x3c, 0x3a, 0xe4, 0x69, - 0x3a, 0x8c, 0x91, 0xcc, 0xdb, 0xc7, 0x2b, 0x76, 0x92, 0xf0, 0x31, 0x49, 0xec, 0x3d, 0x5f, 0x62, - 0x89, 0x90, 0xd1, 0x34, 0xc5, 0xbb, 0xc1, 0x14, 0x93, 0x21, 0x43, 0x7f, 0xb2, 0xf9, 0x49, 0xb2, - 0xa9, 0x90, 0x95, 0x97, 0x76, 0x31, 0x92, 0xf6, 0x4a, 0xc8, 0x32, 0x48, 0x80, 0x7c, 0x08, 0xd0, - 0xe0, 0xbf, 0x44, 0x95, 0x72, 0x78, 0xd1, 0xe4, 0x4f, 0x60, 0xcb, 0xd3, 0x7a, 0x89, 0xdf, 0x81, - 0x8c, 0xf7, 0xd3, 0x15, 0xae, 0xa2, 0xa7, 0xea, 0x44, 0x25, 0xf7, 0x61, 0xb3, 0x11, 0xf8, 0x85, - 0x43, 0xf7, 0x60, 0xdd, 0xec, 0xeb, 0xed, 0x78, 0xe7, 0xac, 0xd9, 0xd7, 0x3d, 0x1f, 0x62, 0x3d, - 0xc4, 0x6f, 0xa6, 0xd6, 0x89, 0x88, 0xf5, 0x10, 0xbf, 0xf1, 0xac, 0x65, 0x19, 0xd6, 0x79, 0xbb, - 0xe8, 0x6a, 0x75, 0xd7, 0x42, 0xc8, 0x5f, 0xac, 0xbc, 0x40, 0xcb, 0xb0, 0x5e, 0x1d, 0xf5, 0x5d, - 0xe3, 0x4c, 0xeb, 0xb8, 0xa6, 0xed, 0xa0, 0x8f, 0x21, 0x35, 0xe8, 0x6a, 0xde, 0x3d, 0x23, 0x3f, - 0xb3, 0x1d, 0xfa, 0x1c, 0x54, 0x6a, 0x2d, 0xbb, 0x90, 0xf5, 0x09, 0xd1, 0x87, 0x90, 0x72, 0xc7, - 0x16, 0x9b, 0x68, 0xf3, 0xe4, 0x70, 0x76, 0x90, 0xae, 0xd6, 0x1a, 0x5b, 0x58, 0xa5, 0x96, 0xe8, - 0xa3, 0xe0, 0x61, 0xfd, 0xfa, 0x6c, 0x97, 0xb3, 0x92, 0xff, 0xac, 0x2e, 0x7f, 0x23, 0xc0, 0x26, - 0x4b, 0x4d, 0xc5, 0x8e, 0x65, 0x0e, 0x9d, 0xc0, 0xfd, 0x42, 0x08, 0xdc, 0x2f, 0x44, 0x48, 0x8e, - 0x6c, 0xaf, 0x29, 0x93, 0x4f, 0xb2, 0x47, 0x1c, 0xdc, 0xb1, 0xb1, 0xcb, 0xb7, 0x17, 0x1f, 0x4d, - 0xa1, 0xa4, 0xde, 0x01, 0x8a, 0x0a, 0x9b, 0xf5, 0x4a, 0xf9, 0xf4, 0xb4, 0x6f, 0xe0, 0xa1, 0x5b, - 0x1a, 0xb9, 0x17, 0xe4, 0x74, 0xd0, 0xa1, 0xa3, 0x29, 0x96, 0x0c, 0x13, 0x54, 0x74, 0x74, 0x1b, - 0x36, 0xb8, 0x92, 0x43, 0x60, 0xb8, 0xd6, 0x99, 0xb0, 0x49, 0x65, 0xf2, 0xef, 0x04, 0xd8, 0xaf, - 0x8e, 0x1b, 0xb6, 0xf9, 0x05, 0xee, 0xb8, 0x75, 0xbb, 0xd7, 0xc4, 0x9a, 0xdd, 0xb9, 0xf0, 0x2a, - 0x6e, 0x17, 0xd2, 0x66, 0xb7, 0xeb, 0x60, 0x97, 0x06, 0x4f, 0xa9, 0x7c, 0x44, 0x9a, 0x68, 0xdf, - 0x18, 0x18, 0x2c, 0x64, 0x4a, 0x65, 0x03, 0x92, 0xbe, 0xe6, 0x74, 0x68, 0x4a, 0x19, 0x95, 0x7c, - 0xa2, 0x33, 0x58, 0x7d, 0x3d, 0xc2, 0xb6, 0x81, 0xc9, 0x66, 0x20, 0x6b, 0x7d, 0x6f, 0x76, 0xa2, - 0x11, 0x00, 0x9f, 0x8d, 0xb0, 0x3d, 0x56, 0x3d, 0x67, 0xf9, 0x8f, 0x02, 0xec, 0xc5, 0x18, 0xa1, - 0x33, 0x48, 0x7e, 0x89, 0xc7, 0xbc, 0x0c, 0x0a, 0x4b, 0xc6, 0x7f, 0x86, 0xc7, 0x74, 0x63, 0x7e, - 0x2d, 0x24, 0xf2, 0x57, 0x54, 0x12, 0x00, 0x3d, 0x84, 0xf4, 0x00, 0xbb, 0x17, 0xa6, 0xce, 0xcb, - 0xe3, 0xd6, 0xcc, 0x50, 0xcc, 0xbd, 0x4a, 0x0d, 0x55, 0xee, 0x40, 0xe8, 0xb8, 0xd4, 0xfa, 0x23, - 0xef, 0x6c, 0xc7, 0x06, 0xf2, 0x6f, 0x05, 0xc8, 0xcd, 0xa2, 0x96, 0x57, 0xd1, 0xbb, 0x71, 0x7b, - 0x0b, 0xd6, 0x5d, 0x93, 0x74, 0x2a, 0x1b, 0x3b, 0xa3, 0x3e, 0x2b, 0xa7, 0x94, 0x9a, 0xa5, 0x32, - 0x95, 0x8a, 0xd0, 0x87, 0xa4, 0x45, 0x51, 0x65, 0x8a, 0x72, 0x2d, 0xcd, 0x4c, 0xa0, 0x6e, 0xf7, - 0x54, 0x6e, 0x27, 0xdf, 0x83, 0xad, 0x8a, 0x53, 0xd2, 0x07, 0xc6, 0x70, 0x82, 0x6a, 0x1f, 0x32, - 0x86, 0xd3, 0xd6, 0x88, 0x8c, 0xe2, 0xca, 0xa8, 0xab, 0x06, 0x33, 0x91, 0xef, 0x42, 0xb2, 0x6e, - 0xf7, 0x22, 0xbf, 0x2e, 0x08, 0x52, 0xbe, 0x53, 0x2f, 0xfd, 0x96, 0x1f, 0xc0, 0x46, 0x75, 0xdc, - 0xc0, 0xf6, 0xc0, 0x60, 0xef, 0x02, 0x28, 0x0f, 0x59, 0x6b, 0x3a, 0xa4, 0x1b, 0x7f, 0x4d, 0xf5, - 0x8b, 0x0a, 0x76, 0xe0, 0x61, 0x82, 0x5d, 0xe4, 0xf3, 0x70, 0xf8, 0xbc, 0xa9, 0xa8, 0x4d, 0xa5, - 0xd9, 0xac, 0xd4, 0x6b, 0xcd, 0x56, 0xa9, 0xa5, 0xb4, 0x9f, 0xd7, 0x9a, 0x0d, 0xe5, 0xb4, 0x72, - 0x56, 0x51, 0xca, 0xe2, 0x15, 0x74, 0x00, 0x7b, 0x11, 0x8b, 0xd2, 0x69, 0xab, 0xf2, 0x42, 0x11, - 0x05, 0x74, 0x13, 0x0e, 0x22, 0xca, 0x96, 0xa2, 0x56, 0x2b, 0xb5, 0x52, 0x4b, 0x29, 0x8b, 0x89, - 0xc2, 0x6b, 0x10, 0xc9, 0x86, 0xf2, 0x92, 0x27, 0xad, 0x02, 0xed, 0xc3, 0x35, 0x2a, 0x53, 0x9a, - 0x8d, 0x7a, 0xad, 0xa9, 0xb4, 0x5e, 0x36, 0x94, 0xf6, 0x69, 0xbd, 0xac, 0x88, 0x57, 0xd0, 0x75, - 0xd8, 0x8f, 0xa8, 0x2a, 0xe5, 0x76, 0xab, 0xfe, 0x4c, 0xa9, 0x89, 0x02, 0xba, 0x0d, 0x37, 0x63, - 0xd5, 0xdc, 0x28, 0x51, 0xf8, 0xbd, 0xc0, 0x0e, 0x27, 0x2c, 0xc1, 0x1c, 0xec, 0x52, 0x84, 0xfe, - 0xcc, 0x14, 0x9e, 0xda, 0x55, 0x10, 0xa7, 0xba, 0x49, 0x4e, 0xbb, 0x80, 0xa6, 0xd2, 0x4a, 0x8d, - 0xcb, 0x13, 0xe8, 0x1a, 0x6c, 0x4f, 0xe5, 0x65, 0xe5, 0x5c, 0x21, 0x19, 0x26, 0x83, 0x41, 0xce, - 0xeb, 0xa7, 0xcf, 0x94, 0xb2, 0x98, 0x0a, 0x1a, 0x37, 0x9f, 0x37, 0x1b, 0x4a, 0xad, 0x2c, 0xae, - 0x04, 0xc5, 0x95, 0x5a, 0xa5, 0x55, 0x29, 0x9d, 0x8b, 0xe9, 0xc2, 0x8f, 0x21, 0xcd, 0xce, 0xa6, - 0x64, 0xf2, 0x27, 0x4a, 0xad, 0xac, 0xa8, 0xa1, 0x55, 0xd8, 0x86, 0x0d, 0x2e, 0x3f, 0x53, 0xaa, - 0xa5, 0x73, 0x82, 0x73, 0x0b, 0xb2, 0x5c, 0x44, 0x05, 0x09, 0x84, 0x60, 0x93, 0x0b, 0xca, 0x95, - 0x17, 0x64, 0x51, 0xc4, 0x64, 0xa1, 0x0c, 0xab, 0xbc, 0x43, 0xa3, 0x3d, 0xd8, 0xa9, 0x9e, 0x95, - 0x28, 0x67, 0xc1, 0xd8, 0x5b, 0x90, 0xf5, 0x14, 0xcd, 0x6a, 0x93, 0x45, 0xf6, 0x04, 0xf5, 0x56, - 0x43, 0x4c, 0x14, 0xba, 0x90, 0xf1, 0x3a, 0x25, 0x92, 0xe0, 0x2a, 0xf9, 0x9e, 0x51, 0x29, 0xbb, - 0x80, 0x26, 0x9a, 0x5a, 0xbd, 0xd5, 0x56, 0x95, 0x52, 0xf9, 0xa5, 0x28, 0x10, 0x5c, 0x13, 0x39, - 0x93, 0x25, 0x08, 0x6b, 0x3e, 0x59, 0xb5, 0xfe, 0x82, 0x70, 0x59, 0x78, 0x05, 0xd7, 0x66, 0x36, - 0x12, 0x74, 0x07, 0x6e, 0x55, 0x5f, 0x36, 0xd4, 0xfa, 0x0f, 0x95, 0xd3, 0x56, 0x5d, 0x7d, 0xd2, - 0x54, 0x4a, 0xea, 0xe9, 0xd3, 0x67, 0xca, 0xcb, 0x10, 0x02, 0x19, 0x6e, 0xcc, 0x36, 0xab, 0xab, - 0x4f, 0xda, 0xb5, 0x52, 0x55, 0x11, 0x85, 0xc2, 0x4f, 0x61, 0xdd, 0xdf, 0x61, 0x08, 0x2d, 0xcc, - 0xae, 0xaa, 0xb4, 0x9e, 0xd6, 0xcb, 0x6d, 0xe5, 0xb3, 0xe7, 0xa5, 0xf3, 0xa6, 0x78, 0x05, 0x1d, - 0x82, 0x14, 0x50, 0x34, 0x5b, 0x25, 0xb5, 0xd5, 0x6c, 0xff, 0xa8, 0xd2, 0x7a, 0x2a, 0x0a, 0xa4, - 0x88, 0x03, 0xda, 0xd3, 0x7a, 0xad, 0x55, 0xaa, 0xd4, 0x9a, 0x62, 0xe2, 0xe4, 0xaf, 0xfb, 0x90, - 0x25, 0xbf, 0x1d, 0x4d, 0x6c, 0x5f, 0x1a, 0x1d, 0x8c, 0x9e, 0xc1, 0xea, 0x53, 0xac, 0xf5, 0xdd, - 0x8b, 0xaf, 0xd0, 0x6e, 0xe4, 0xf9, 0x45, 0x19, 0x58, 0xee, 0x38, 0x17, 0x23, 0x97, 0xc5, 0xaf, - 0xff, 0xf1, 0xef, 0x5f, 0x27, 0x00, 0x65, 0x8a, 0x17, 0x3c, 0xc2, 0x13, 0x58, 0x51, 0xb1, 0xa6, - 0x8f, 0xdf, 0x39, 0xd4, 0x26, 0x0d, 0x95, 0x41, 0xe9, 0xa2, 0x4d, 0xfd, 0x6b, 0x90, 0x79, 0xc1, - 0x1f, 0x40, 0x63, 0x63, 0xed, 0x45, 0xe4, 0x4d, 0xfa, 0xd6, 0x2a, 0x6f, 0xd3, 0x60, 0x59, 0xb4, - 0x36, 0x79, 0x44, 0x45, 0x3f, 0x83, 0xed, 0x27, 0xd8, 0x65, 0x37, 0x1a, 0xef, 0xb1, 0x32, 0x36, - 0xf0, 0x9d, 0x65, 0x1e, 0x3e, 0x1d, 0xf9, 0x83, 0xaf, 0xff, 0x24, 0x6d, 0xc1, 0x06, 0xb1, 0xc1, - 0x43, 0xd7, 0xe8, 0x68, 0x2e, 0xd6, 0xe9, 0xcc, 0x08, 0x89, 0xc5, 0x01, 0x2e, 0x92, 0x43, 0x81, - 0xf7, 0xa4, 0x8a, 0xbe, 0x02, 0x71, 0x02, 0xc0, 0x7b, 0x44, 0x88, 0x9b, 0x3f, 0x1f, 0x3b, 0x3f, - 0xf7, 0x94, 0xef, 0xc5, 0x4d, 0xbd, 0x83, 0xb6, 0xd9, 0xbc, 0x04, 0x80, 0xc5, 0xe7, 0xf9, 0x8d, - 0x00, 0x3b, 0xec, 0x34, 0x1d, 0x9c, 0xff, 0xfe, 0xec, 0x79, 0x62, 0x6e, 0xeb, 0x4b, 0xc0, 0x2a, - 0xc6, 0xc1, 0xda, 0xcd, 0x45, 0x61, 0x3d, 0x12, 0x0a, 0xc8, 0x85, 0xcd, 0x09, 0x2b, 0xec, 0xa6, - 0x1b, 0xc7, 0x49, 0xfc, 0xcb, 0x2a, 0xf5, 0x93, 0x0b, 0x71, 0x53, 0x6f, 0xa3, 0xad, 0xe9, 0xd4, - 0xec, 0x9e, 0xfc, 0xad, 0x00, 0xdb, 0xec, 0x64, 0xec, 0x9f, 0xf9, 0x83, 0x05, 0x6c, 0xf8, 0x2f, - 0xa1, 0x0b, 0xe1, 0xdc, 0x8f, 0x83, 0x73, 0x35, 0x17, 0x86, 0x43, 0x78, 0xf8, 0x95, 0x00, 0xdb, - 0x91, 0x4b, 0x77, 0xcc, 0xfa, 0xc4, 0x5d, 0xce, 0x63, 0xf7, 0xd6, 0xf7, 0xe2, 0xb0, 0x1c, 0xca, - 0x7b, 0x21, 0x2c, 0x45, 0x76, 0xf9, 0x1d, 0x13, 0x4c, 0xdf, 0x09, 0x70, 0x5d, 0xc5, 0x0e, 0x1e, - 0xea, 0xd5, 0xb1, 0xef, 0xf5, 0xa0, 0x43, 0x1f, 0x90, 0xab, 0xf3, 0xd6, 0x2a, 0x0e, 0x48, 0x29, - 0x0e, 0xc8, 0x91, 0x7c, 0x3b, 0x02, 0xc4, 0xa6, 0x53, 0x5f, 0xfa, 0xe6, 0x0c, 0x17, 0x0c, 0xbb, - 0xdf, 0xbf, 0x7b, 0xc1, 0x50, 0xbf, 0x25, 0x0b, 0x86, 0xbd, 0x0e, 0x84, 0x0b, 0x86, 0xcd, 0xbc, - 0xa8, 0x60, 0xfc, 0x77, 0xf8, 0x85, 0x70, 0x96, 0x2b, 0x18, 0x0a, 0x87, 0xf0, 0xf0, 0x6d, 0xa8, - 0x60, 0xe6, 0x21, 0x9a, 0xfd, 0xaa, 0xf0, 0x3f, 0x96, 0x0b, 0x45, 0x12, 0x57, 0x2e, 0xbe, 0xf7, - 0x12, 0xb6, 0x74, 0xec, 0xbd, 0xe0, 0xff, 0x52, 0x2e, 0x1c, 0xc8, 0xec, 0x72, 0xf1, 0x77, 0x5d, - 0xef, 0xdd, 0xe1, 0xdd, 0xbb, 0xae, 0x77, 0x61, 0x5f, 0xae, 0xeb, 0xf2, 0xdb, 0x7f, 0xa4, 0xeb, - 0x7a, 0xf3, 0x2f, 0xea, 0xba, 0xc1, 0xd7, 0x8e, 0x25, 0x60, 0x2d, 0xd7, 0x75, 0x39, 0x2c, 0xc2, - 0xca, 0x6b, 0x58, 0xa3, 0xac, 0x54, 0xbb, 0x5a, 0x3c, 0x1d, 0xb7, 0x16, 0xdd, 0xca, 0x1d, 0xf9, - 0x6e, 0xdc, 0xc4, 0x22, 0xda, 0x9c, 0x4e, 0x4c, 0xee, 0xee, 0xe8, 0xe7, 0x02, 0x6c, 0x34, 0xc9, - 0x9c, 0x93, 0x57, 0x86, 0xd9, 0x7f, 0x5d, 0x0c, 0x3d, 0x79, 0xc4, 0xd6, 0xc6, 0x83, 0xb8, 0xa9, - 0xa5, 0xdc, 0x8e, 0xaf, 0x36, 0x78, 0x34, 0x9a, 0xf5, 0x2f, 0x05, 0x10, 0xbd, 0x4d, 0x3c, 0x41, - 0x71, 0x7b, 0x2e, 0x0a, 0x66, 0x1e, 0x0b, 0xe2, 0x61, 0x1c, 0x88, 0x7c, 0xee, 0x60, 0x06, 0x88, - 0x22, 0xff, 0x7b, 0x04, 0x01, 0x33, 0x86, 0xb5, 0x92, 0xae, 0x57, 0xbb, 0x5a, 0xbd, 0xd5, 0x88, - 0x5d, 0x82, 0xdb, 0x71, 0x6f, 0x1a, 0xbe, 0xc7, 0x88, 0x39, 0xab, 0x2f, 0x6f, 0x07, 0x16, 0xa1, - 0x68, 0xba, 0x16, 0x6f, 0x1d, 0xbe, 0x17, 0x9b, 0x56, 0x03, 0xdd, 0x9a, 0xf7, 0x33, 0x43, 0x27, - 0x5b, 0x0e, 0xc9, 0xf7, 0xe3, 0x90, 0xdc, 0xc8, 0xed, 0x47, 0x90, 0xf8, 0x5b, 0x47, 0x1f, 0xd6, - 0x55, 0x3c, 0x30, 0x2f, 0xf1, 0x02, 0x3e, 0xe2, 0xd6, 0x21, 0x7e, 0x5f, 0x16, 0xa2, 0x14, 0xa0, - 0x3f, 0x08, 0xb0, 0xc3, 0xcf, 0xd8, 0xbe, 0xd3, 0xbc, 0x83, 0x8e, 0x97, 0x7c, 0x3a, 0xf0, 0x4a, - 0xb3, 0xb8, 0xb4, 0x3d, 0xe7, 0x27, 0xbe, 0x5c, 0xe4, 0x83, 0x62, 0xaf, 0x6f, 0xbe, 0xd2, 0xfa, - 0xe4, 0x6c, 0x44, 0x9c, 0x4d, 0xbb, 0xe7, 0x14, 0xdb, 0x0e, 0xf5, 0x67, 0x7d, 0x0c, 0x2a, 0x4e, - 0x45, 0x1b, 0xd0, 0x8b, 0x78, 0x2c, 0x3f, 0xb3, 0xb7, 0x54, 0xe8, 0x86, 0x2f, 0x1f, 0xc7, 0xc1, - 0xb8, 0x86, 0x76, 0x3c, 0x18, 0x6d, 0xc3, 0x31, 0xb4, 0x01, 0x7d, 0x05, 0x40, 0xbf, 0x10, 0x60, - 0x8f, 0xb6, 0x8b, 0xcf, 0x59, 0x70, 0xff, 0xb5, 0x3e, 0x0e, 0x89, 0x1c, 0xc7, 0xcd, 0xd4, 0x57, - 0x3e, 0x89, 0xc3, 0xb1, 0x8f, 0xf6, 0x8a, 0xbe, 0x97, 0x81, 0x22, 0x0f, 0x55, 0x1c, 0xe0, 0xc7, - 0x7f, 0x11, 0xbe, 0x2b, 0x7d, 0x23, 0xa0, 0x4f, 0x21, 0x43, 0xae, 0x30, 0xf9, 0x52, 0xa3, 0x22, - 0x17, 0xd0, 0xd1, 0x85, 0xeb, 0x5a, 0xce, 0xa3, 0x62, 0xb1, 0x67, 0xb8, 0x17, 0xa3, 0x57, 0xc7, - 0x1d, 0x73, 0x50, 0xec, 0x68, 0xe6, 0xd4, 0xd1, 0xfa, 0xb2, 0x57, 0x24, 0xd3, 0x9c, 0x24, 0x3f, - 0x3c, 0x7e, 0x50, 0x10, 0x12, 0x27, 0xa2, 0x66, 0x59, 0x7d, 0xfe, 0x33, 0x51, 0xfc, 0xc2, 0x31, - 0x87, 0x41, 0x49, 0xcf, 0xb6, 0x3a, 0x8f, 0x22, 0x36, 0x8f, 0x22, 0x36, 0x9f, 0xdf, 0x5d, 0x34, - 0x23, 0xfd, 0x1f, 0x24, 0xc4, 0xf4, 0x55, 0x9a, 0x32, 0xf4, 0xd1, 0x7f, 0x03, 0x00, 0x00, 0xff, - 0xff, 0x66, 0xd3, 0x09, 0x6b, 0x7c, 0x22, 0x00, 0x00, + // 2779 bytes of a gzipped FileDescriptorProto + 0x1f, 0x8b, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0xff, 0xcc, 0x5a, 0x4b, 0x6f, 0x1b, 0xd7, + 0x15, 0xf6, 0x90, 0x14, 0x45, 0x1e, 0x4a, 0xd4, 0xe8, 0xca, 0x96, 0x46, 0x0f, 0xdb, 0xf4, 0x38, + 0x46, 0x64, 0xc6, 0x16, 0x13, 0x25, 0x45, 0x63, 0x67, 0x51, 0xd0, 0xe2, 0xc8, 0x66, 0x2d, 0x3e, + 0x32, 0xa4, 0xdd, 0x3a, 0x40, 0xc1, 0x8e, 0x39, 0x57, 0xd4, 0x24, 0x24, 0x67, 0x3c, 0x33, 0x94, + 0xc1, 0x6c, 0x0a, 0x78, 0x95, 0x76, 0x13, 0x34, 0x5d, 0x74, 0xd9, 0x45, 0x77, 0x5d, 0xb5, 0x45, + 0x1f, 0x7f, 0xa1, 0x9b, 0xae, 0xda, 0x5f, 0x50, 0xf4, 0x2f, 0x14, 0x05, 0xb2, 0x28, 0x8a, 0xfb, + 0x18, 0x72, 0x1e, 0x1c, 0x91, 0x82, 0x51, 0xa0, 0x2b, 0xf3, 0x9e, 0xe7, 0x77, 0xcf, 0x39, 0xf7, + 0xcc, 0xbd, 0xc7, 0x02, 0xd0, 0x46, 0xee, 0xd9, 0x81, 0x65, 0x9b, 0xae, 0x89, 0x36, 0xbe, 0x34, + 0x5c, 0x4d, 0xc7, 0xfd, 0x03, 0x4a, 0xd3, 0x2c, 0xe3, 0xe0, 0xfc, 0x83, 0x9d, 0xbd, 0x9e, 0x69, + 0xf6, 0xfa, 0xb8, 0xa4, 0x59, 0x46, 0x49, 0x1b, 0x0e, 0x4d, 0x57, 0x73, 0x0d, 0x73, 0xe8, 0x30, + 0x95, 0x9d, 0x5d, 0xce, 0xa5, 0xab, 0x97, 0xa3, 0xd3, 0x12, 0x1e, 0x58, 0xee, 0x98, 0x33, 0xf7, + 0xc2, 0x4c, 0xc7, 0xb5, 0x47, 0x5d, 0x97, 0x73, 0x6f, 0x86, 0xb9, 0xae, 0x31, 0xc0, 0x8e, 0xab, + 0x0d, 0x2c, 0x2e, 0xb0, 0x75, 0xae, 0xf5, 0x0d, 0x5d, 0x73, 0x71, 0xc9, 0xfb, 0xc1, 0x19, 0xf7, + 0xe8, 0x3f, 0xdd, 0xfb, 0x3d, 0x3c, 0xbc, 0xef, 0xbc, 0xd6, 0x7a, 0x3d, 0x6c, 0x97, 0x4c, 0x8b, + 0xc2, 0x9a, 0x01, 0x51, 0x22, 0xbb, 0x61, 0x6c, 0x4f, 0x8a, 0x71, 0xe4, 0x1f, 0x81, 0xf8, 0xcc, + 0xc1, 0x76, 0x0b, 0x3b, 0x8e, 0x61, 0x0e, 0x9f, 0x1b, 0xf8, 0xb5, 0x83, 0xaa, 0xb0, 0x3a, 0x72, + 0xb0, 0xdd, 0x71, 0x18, 0xd1, 0x91, 0x84, 0x42, 0x72, 0x3f, 0x77, 0xf8, 0xce, 0xc1, 0x8c, 0xd8, + 0x1c, 0x84, 0xb4, 0xd5, 0x95, 0xd1, 0x94, 0xe0, 0xc8, 0x7f, 0x17, 0x60, 0x2d, 0x24, 0x81, 0xf2, + 0x90, 0x30, 0x74, 0x49, 0x28, 0x08, 0xfb, 0x59, 0x35, 0x61, 0xe8, 0x68, 0x1b, 0x32, 0x5a, 0x0f, + 0x0f, 0xdd, 0x8e, 0xa1, 0x4b, 0x09, 0x4a, 0x5d, 0xa6, 0xeb, 0xaa, 0x8e, 0x2a, 0x2c, 0x37, 0x1d, + 0xc7, 0xd5, 0x5c, 0x2c, 0x25, 0x0b, 0xc2, 0x7e, 0xfe, 0xf0, 0xce, 0x3c, 0x18, 0x2d, 0x22, 0xac, + 0x66, 0x09, 0x97, 0xfe, 0x44, 0x5b, 0xb0, 0x4c, 0xf7, 0x63, 0xe8, 0x52, 0x8a, 0xda, 0x4f, 0x93, + 0x65, 0x55, 0x47, 0xbb, 0x90, 0xa5, 0x8c, 0xa1, 0x36, 0xc0, 0xd2, 0x12, 0x65, 0x65, 0x08, 0xa1, + 0xae, 0x0d, 0x30, 0xda, 0x81, 0x8c, 0x83, 0x5f, 0x8d, 0xf0, 0xb0, 0x8b, 0xa5, 0x74, 0x41, 0xd8, + 0x4f, 0xa9, 0x93, 0xb5, 0xfc, 0x9f, 0x65, 0x48, 0x11, 0x8f, 0x91, 0xbd, 0x7c, 0x04, 0x4b, 0x0c, + 0x6b, 0x82, 0x62, 0xbd, 0x11, 0x8f, 0x95, 0x82, 0x64, 0xc2, 0xe8, 0x7b, 0xb0, 0xda, 0xb5, 0x31, + 0xcd, 0x58, 0x47, 0xf7, 0x76, 0x9a, 0x3b, 0xdc, 0x39, 0x60, 0xe5, 0x71, 0xe0, 0x95, 0xc7, 0x41, + 0xdb, 0x2b, 0x0f, 0x75, 0xc5, 0x53, 0xa8, 0x10, 0x03, 0x47, 0xb0, 0xa6, 0x75, 0x5d, 0xe3, 0xdc, + 0x67, 0x22, 0x35, 0xd7, 0x44, 0x7e, 0xaa, 0x42, 0x8d, 0x7c, 0x02, 0xb9, 0xee, 0x99, 0x36, 0xec, + 0x61, 0x66, 0x60, 0x69, 0xae, 0x01, 0x60, 0xe2, 0x54, 0xf9, 0x01, 0x40, 0x5f, 0x73, 0xdc, 0x4e, + 0xdf, 0xec, 0x19, 0x43, 0x1a, 0xaf, 0x8b, 0x75, 0xb3, 0x44, 0xfa, 0x84, 0x08, 0x23, 0x05, 0x44, + 0x4b, 0x73, 0x9c, 0xd7, 0xa6, 0xad, 0x77, 0x98, 0x45, 0x5d, 0x5a, 0x9e, 0x6b, 0x60, 0xcd, 0xd3, + 0x39, 0x62, 0x2a, 0xc1, 0x64, 0x66, 0x42, 0xc9, 0xbc, 0x0e, 0x70, 0x6a, 0xd8, 0x8e, 0xcb, 0xb8, + 0x59, 0xca, 0xcd, 0x52, 0x0a, 0x65, 0xef, 0x02, 0xc5, 0xc3, 0xb8, 0xc0, 0x74, 0x09, 0xc1, 0x63, + 0x0e, 0x8d, 0xee, 0x17, 0x8c, 0x99, 0x63, 0x4c, 0x42, 0xa0, 0xcc, 0x5b, 0xb0, 0xa2, 0x1b, 0x8e, + 0xd5, 0xd7, 0xc6, 0x8c, 0xbf, 0x42, 0xf9, 0x39, 0x4e, 0xa3, 0x22, 0xf7, 0x01, 0x59, 0x36, 0x3e, + 0xc5, 0xb6, 0x8d, 0xf5, 0x4e, 0x5f, 0x1b, 0xf6, 0x46, 0x5a, 0x0f, 0x4b, 0xab, 0x54, 0x70, 0x7d, + 0xc2, 0x39, 0xe1, 0x0c, 0xf4, 0x21, 0xa4, 0x7b, 0x78, 0xa8, 0x63, 0x5b, 0xca, 0xd3, 0x1a, 0xda, + 0x9d, 0x59, 0x43, 0x8f, 0xa9, 0x88, 0xca, 0x45, 0xd1, 0x55, 0x58, 0xc2, 0x03, 0xcd, 0xe8, 0x4b, + 0x6b, 0xd4, 0x2c, 0x5b, 0xa0, 0x22, 0xac, 0x1b, 0x4e, 0x87, 0xfe, 0xee, 0x9c, 0x63, 0xdb, 0x38, + 0x35, 0xb0, 0x2e, 0x89, 0x05, 0x61, 0x3f, 0xa3, 0xae, 0x19, 0x8e, 0x42, 0xe8, 0xcf, 0x39, 0x99, + 0x58, 0xb0, 0xce, 0xcc, 0x21, 0x96, 0xd6, 0x99, 0x05, 0xba, 0xe0, 0x16, 0xe8, 0xef, 0xa9, 0x05, + 0xe4, 0x59, 0x68, 0x12, 0xfa, 0xc4, 0x82, 0x04, 0xcb, 0x5d, 0x73, 0x34, 0x74, 0xed, 0xb1, 0xb4, + 0xc1, 0x8e, 0x31, 0x5f, 0x92, 0xa3, 0xd4, 0x37, 0xbb, 0x5a, 0xdf, 0x70, 0xc7, 0xd2, 0x55, 0x1e, + 0x5d, 0xbe, 0x46, 0x37, 0x21, 0x67, 0x99, 0x8e, 0xab, 0xf5, 0x3b, 0x5d, 0x53, 0xc7, 0xd2, 0x35, + 0xca, 0x06, 0x46, 0x3a, 0x32, 0x75, 0x8c, 0x36, 0x21, 0x6d, 0xe3, 0x9e, 0x61, 0x0e, 0xa5, 0x4d, + 0x76, 0x78, 0xd9, 0x0a, 0xdd, 0x81, 0xbc, 0xe3, 0xda, 0x18, 0xbb, 0x1d, 0x4d, 0xd7, 0x6d, 0xec, + 0x38, 0xd2, 0x16, 0xe5, 0xaf, 0x32, 0x6a, 0x99, 0x11, 0xd1, 0xc7, 0x20, 0x85, 0xaa, 0xab, 0x63, + 0xe3, 0x57, 0x23, 0xc3, 0xc6, 0xba, 0x24, 0xd1, 0x8d, 0x6c, 0x06, 0x2b, 0x49, 0xe5, 0xdc, 0x40, + 0x03, 0xd8, 0x0e, 0x35, 0x80, 0x3f, 0x24, 0x21, 0x47, 0x8e, 0x71, 0xd3, 0x36, 0x4f, 0x8d, 0x3e, + 0x8e, 0xf4, 0x81, 0x40, 0x31, 0x26, 0x2e, 0x2c, 0xc6, 0xe4, 0x85, 0xc5, 0x98, 0xba, 0xa8, 0x18, + 0x97, 0xe6, 0x14, 0x63, 0x7a, 0xd1, 0x62, 0x5c, 0x9e, 0x5f, 0x8c, 0x99, 0xc5, 0x8b, 0xd1, 0x1f, + 0xb8, 0x6c, 0x30, 0x70, 0xd1, 0x56, 0x07, 0x97, 0x6c, 0x75, 0xa1, 0x2e, 0x95, 0xbb, 0x4c, 0x97, + 0x92, 0xff, 0x94, 0x00, 0xe9, 0x99, 0x45, 0x14, 0x7d, 0xc9, 0x23, 0x09, 0xc7, 0x8e, 0x8b, 0xee, + 0x06, 0xd2, 0x42, 0x73, 0xf9, 0x08, 0xbe, 0x7d, 0xb4, 0x6c, 0x2f, 0x89, 0x82, 0xf4, 0x17, 0xc1, + 0x9f, 0xa2, 0x77, 0xfd, 0x29, 0x4a, 0x44, 0x24, 0xa7, 0xe9, 0x7a, 0xd7, 0x9f, 0xae, 0x64, 0x54, + 0x70, 0x92, 0xba, 0xfb, 0xa1, 0xd4, 0xa5, 0x22, 0xb2, 0x81, 0x34, 0x3e, 0x98, 0x99, 0xc6, 0xa5, + 0x88, 0xd2, 0x85, 0x29, 0x4d, 0x2f, 0x9c, 0x52, 0xf9, 0x5f, 0x02, 0x64, 0x49, 0xc8, 0x68, 0xcf, + 0x88, 0x54, 0xfb, 0xa4, 0xfb, 0x24, 0xfc, 0xdd, 0x67, 0x1f, 0xc2, 0x4d, 0x86, 0x46, 0x60, 0x46, + 0xef, 0xf1, 0x17, 0x4c, 0x6a, 0x5e, 0xc1, 0x2c, 0xbd, 0x5d, 0xc1, 0xa4, 0x2f, 0x55, 0x30, 0x0f, + 0x41, 0xa2, 0x28, 0xc7, 0xb5, 0xf1, 0x64, 0xfb, 0x5e, 0xbd, 0xdc, 0x80, 0x14, 0x6d, 0x59, 0xd1, + 0x4a, 0xa1, 0x74, 0xf9, 0x09, 0x6c, 0x32, 0xdd, 0x88, 0x66, 0x38, 0x7e, 0x9e, 0xa5, 0x44, 0x8c, + 0xa5, 0x87, 0xb0, 0x39, 0xad, 0xda, 0x80, 0xa5, 0x82, 0x17, 0xf9, 0x28, 0x08, 0xc6, 0x90, 0xff, + 0xcd, 0x33, 0x47, 0x7b, 0xf5, 0xac, 0xcc, 0xb1, 0xae, 0x9f, 0x98, 0xdb, 0xf5, 0x93, 0xb3, 0xbb, + 0xfe, 0xff, 0x6f, 0xee, 0x1e, 0xf8, 0xa3, 0x46, 0x41, 0x7b, 0x51, 0xbb, 0xe9, 0xed, 0x9a, 0x45, + 0x2d, 0xfb, 0xed, 0xa3, 0xb4, 0x9d, 0x12, 0x05, 0xe9, 0x2a, 0x0f, 0x80, 0xfc, 0xb1, 0x3f, 0x75, + 0x01, 0xd5, 0x79, 0x49, 0xff, 0x6b, 0x82, 0x7d, 0x18, 0xbc, 0xcf, 0x4f, 0x38, 0xe0, 0xbe, 0x8f, + 0x64, 0x22, 0xfe, 0x23, 0x99, 0xbc, 0xf8, 0x23, 0x99, 0xba, 0xe0, 0x23, 0xb9, 0x34, 0xe7, 0x23, + 0x99, 0x9e, 0xf5, 0x91, 0xf4, 0x27, 0x71, 0x79, 0x5e, 0x12, 0x33, 0x6f, 0x97, 0xc4, 0xec, 0xa5, + 0x92, 0xf8, 0x0f, 0xc1, 0xdf, 0xb1, 0x39, 0x5e, 0x2f, 0x19, 0xf2, 0x34, 0x98, 0x2c, 0x1f, 0x99, + 0x6f, 0x1f, 0x2d, 0xd9, 0x49, 0x92, 0x8d, 0x49, 0x58, 0xdf, 0xf1, 0x85, 0x35, 0x11, 0x12, 0x9a, + 0x06, 0xf8, 0x6e, 0x30, 0xc0, 0xc9, 0x90, 0xa0, 0x3f, 0xd4, 0x85, 0x49, 0xa8, 0x53, 0x21, 0x29, + 0x2f, 0xe8, 0xa5, 0x48, 0xd0, 0x97, 0x42, 0x92, 0xc1, 0xf0, 0xcb, 0x7b, 0x00, 0x4d, 0x7e, 0x07, + 0xa9, 0x56, 0xc2, 0x25, 0x23, 0x7f, 0x0c, 0x6b, 0x1e, 0xd7, 0xdb, 0xf8, 0x1d, 0xc8, 0x78, 0x97, + 0x96, 0x70, 0x0d, 0x3f, 0x51, 0x27, 0x2c, 0xb9, 0x0f, 0xf9, 0x66, 0xe0, 0x6e, 0x83, 0xee, 0xc1, + 0x8a, 0xd9, 0xd7, 0x3b, 0xf1, 0xca, 0x39, 0xb3, 0xaf, 0x7b, 0x3a, 0x44, 0x7a, 0x88, 0x5f, 0x4f, + 0xa5, 0x13, 0x11, 0xe9, 0x21, 0x7e, 0xed, 0x49, 0xcb, 0x32, 0xac, 0xf0, 0x5e, 0x79, 0xaa, 0x35, + 0x5c, 0x0b, 0x21, 0xff, 0x51, 0xe1, 0xc7, 0xa3, 0x02, 0x2b, 0xb5, 0x51, 0xdf, 0x35, 0x8e, 0xb5, + 0xae, 0x6b, 0xda, 0x0e, 0xfa, 0x08, 0x52, 0x83, 0x53, 0xcd, 0x7b, 0x61, 0x16, 0x66, 0x7e, 0x8a, + 0x7c, 0x0a, 0x2a, 0x95, 0x96, 0x5d, 0xc8, 0xf9, 0x88, 0xe8, 0x7d, 0x48, 0xb9, 0x63, 0x8b, 0x39, + 0xca, 0x1f, 0xee, 0xcd, 0x36, 0x72, 0xaa, 0xb5, 0xc7, 0x16, 0x56, 0xa9, 0x24, 0xfa, 0x30, 0xf8, + 0x4c, 0xbb, 0x3e, 0x5b, 0xe5, 0xb8, 0xec, 0x7f, 0xa5, 0xc9, 0x5f, 0x09, 0x90, 0x67, 0x5b, 0x53, + 0xb1, 0x63, 0x99, 0x43, 0x27, 0xf0, 0xb2, 0x14, 0x02, 0x2f, 0x4b, 0x11, 0x92, 0x23, 0xdb, 0xfb, + 0x1e, 0x92, 0x9f, 0xe4, 0x84, 0x3a, 0xb8, 0x6b, 0x63, 0x97, 0x1f, 0x6e, 0xbe, 0x9a, 0x42, 0x49, + 0x5d, 0x02, 0x8a, 0x0a, 0xf9, 0x46, 0xb5, 0x72, 0x74, 0xd4, 0x37, 0xf0, 0xd0, 0x2d, 0x8f, 0xdc, + 0x33, 0x72, 0x2f, 0xec, 0xd2, 0xd5, 0x14, 0x4b, 0x86, 0x11, 0xaa, 0x3a, 0xba, 0x0d, 0xab, 0x9c, + 0xc9, 0x21, 0x30, 0x5c, 0x2b, 0x8c, 0xd8, 0xa2, 0x34, 0xf9, 0xd7, 0x02, 0x6c, 0xd7, 0xc6, 0x4d, + 0xdb, 0xfc, 0x1c, 0x77, 0xdd, 0x86, 0xdd, 0x6b, 0x61, 0xcd, 0xee, 0x9e, 0x79, 0x15, 0xb7, 0x09, + 0x69, 0xf3, 0xf4, 0xd4, 0xc1, 0x2e, 0x35, 0x9e, 0x52, 0xf9, 0x8a, 0x7c, 0x40, 0xfa, 0xc6, 0xc0, + 0x60, 0x26, 0x53, 0x2a, 0x5b, 0x90, 0xed, 0x6b, 0x4e, 0x97, 0x6e, 0x29, 0xa3, 0x92, 0x9f, 0xe8, + 0x18, 0x96, 0x5f, 0x8d, 0xb0, 0x6d, 0x60, 0x72, 0x18, 0x48, 0xae, 0xef, 0xcd, 0xde, 0x68, 0x04, + 0xc0, 0xa7, 0x23, 0x6c, 0x8f, 0x55, 0x4f, 0x59, 0xfe, 0xbd, 0x00, 0x5b, 0x31, 0x42, 0xe8, 0x18, + 0x92, 0x5f, 0xe0, 0x31, 0x2f, 0x83, 0xe2, 0x82, 0xf6, 0x9f, 0xe2, 0x31, 0x3d, 0x98, 0x6f, 0x84, + 0x44, 0xe1, 0x8a, 0x4a, 0x0c, 0xa0, 0x07, 0x90, 0x1e, 0x60, 0xf7, 0xcc, 0xd4, 0x79, 0x79, 0xdc, + 0x9a, 0x69, 0x8a, 0xa9, 0xd7, 0xa8, 0xa0, 0xca, 0x15, 0x48, 0x38, 0xce, 0xb5, 0xfe, 0xc8, 0xbb, + 0xd5, 0xb3, 0x85, 0xfc, 0x2b, 0x01, 0x76, 0x66, 0x85, 0x96, 0x57, 0xd1, 0xe5, 0x62, 0x7b, 0x0b, + 0x56, 0x5c, 0x93, 0x74, 0x2a, 0x1b, 0x3b, 0xa3, 0x3e, 0x2b, 0xa7, 0x94, 0x9a, 0xa3, 0x34, 0x95, + 0x92, 0xd0, 0xfb, 0xa4, 0x45, 0x51, 0x66, 0x8a, 0xc6, 0x5a, 0x9a, 0xb9, 0x81, 0x86, 0xdd, 0x53, + 0xb9, 0x9c, 0x7c, 0x0f, 0xd6, 0xaa, 0x4e, 0x59, 0x1f, 0x18, 0xc3, 0x09, 0xaa, 0x6d, 0xc8, 0x18, + 0x4e, 0x47, 0x23, 0x34, 0x8a, 0x2b, 0xa3, 0x2e, 0x1b, 0x4c, 0x44, 0xbe, 0x0b, 0xc9, 0x86, 0xdd, + 0x8b, 0x7c, 0xdb, 0x10, 0xa4, 0x7c, 0xef, 0x1d, 0xfa, 0x5b, 0xfe, 0x00, 0x56, 0x6b, 0xe3, 0x26, + 0xb6, 0x07, 0x06, 0x9b, 0x08, 0xa1, 0x02, 0xe4, 0xac, 0xe9, 0x92, 0x1e, 0xfc, 0xac, 0xea, 0x27, + 0x15, 0xed, 0xc0, 0x48, 0x8a, 0x8d, 0x70, 0x0a, 0xb0, 0xf7, 0xac, 0xa5, 0xa8, 0x2d, 0xa5, 0xd5, + 0xaa, 0x36, 0xea, 0xad, 0x76, 0xb9, 0xad, 0x74, 0x9e, 0xd5, 0x5b, 0x4d, 0xe5, 0xa8, 0x7a, 0x5c, + 0x55, 0x2a, 0xe2, 0x15, 0xb4, 0x0b, 0x5b, 0x11, 0x89, 0xf2, 0x51, 0xbb, 0xfa, 0x5c, 0x11, 0x05, + 0x74, 0x13, 0x76, 0x23, 0xcc, 0xb6, 0xa2, 0xd6, 0xaa, 0xf5, 0x72, 0x5b, 0xa9, 0x88, 0x89, 0xe2, + 0x2b, 0x10, 0xc9, 0x81, 0xf2, 0x36, 0x4f, 0x5a, 0x05, 0xda, 0x86, 0x6b, 0x94, 0xa6, 0xb4, 0x9a, + 0x8d, 0x7a, 0x4b, 0x69, 0xbf, 0x68, 0x2a, 0x9d, 0xa3, 0x46, 0x45, 0x11, 0xaf, 0xa0, 0xeb, 0xb0, + 0x1d, 0x61, 0x55, 0x2b, 0x9d, 0x76, 0xe3, 0xa9, 0x52, 0x17, 0x05, 0x74, 0x1b, 0x6e, 0xc6, 0xb2, + 0xb9, 0x50, 0xa2, 0xf8, 0x5b, 0x7e, 0x31, 0x63, 0x1b, 0xdc, 0x81, 0x4d, 0x8a, 0xd0, 0xbf, 0x33, + 0x85, 0x6f, 0xed, 0x2a, 0x88, 0x53, 0xde, 0x64, 0x4f, 0x9b, 0x80, 0xa6, 0xd4, 0x6a, 0x9d, 0xd3, + 0x13, 0xe8, 0x1a, 0xac, 0x4f, 0xe9, 0x15, 0xe5, 0x44, 0x21, 0x3b, 0x4c, 0x06, 0x8d, 0x9c, 0x34, + 0x8e, 0x9e, 0x2a, 0x15, 0x31, 0x15, 0x14, 0x6e, 0x3d, 0x6b, 0x35, 0x95, 0x7a, 0x45, 0x5c, 0x0a, + 0x92, 0xab, 0xf5, 0x6a, 0xbb, 0x5a, 0x3e, 0x11, 0xd3, 0xc5, 0x1f, 0x42, 0x9a, 0xbd, 0x0b, 0x88, + 0xf3, 0xc7, 0x4a, 0xbd, 0xa2, 0xa8, 0xa1, 0x2c, 0xac, 0xc3, 0x2a, 0xa7, 0x1f, 0x2b, 0xb5, 0xf2, + 0x09, 0xc1, 0xb9, 0x06, 0x39, 0x4e, 0xa2, 0x84, 0x04, 0x42, 0x90, 0xe7, 0x84, 0x4a, 0xf5, 0x39, + 0x49, 0x8a, 0x98, 0x2c, 0x56, 0x60, 0x99, 0x77, 0x68, 0xb4, 0x05, 0x1b, 0xb5, 0xe3, 0x32, 0x8d, + 0x59, 0xd0, 0xf6, 0x1a, 0xe4, 0x3c, 0x46, 0xab, 0xd6, 0x62, 0x96, 0x3d, 0x42, 0xa3, 0xdd, 0x14, + 0x13, 0xc5, 0x53, 0xc8, 0x78, 0x9d, 0x12, 0x49, 0x70, 0x95, 0xfc, 0x9e, 0x51, 0x29, 0x9b, 0x80, + 0x26, 0x9c, 0x7a, 0xa3, 0xdd, 0x51, 0x95, 0x72, 0xe5, 0x85, 0x28, 0x10, 0x5c, 0x13, 0x3a, 0xa3, + 0x25, 0x48, 0xd4, 0x7c, 0xb4, 0x5a, 0xe3, 0x39, 0x89, 0x65, 0xf1, 0x25, 0x5c, 0x9b, 0xd9, 0x48, + 0xd0, 0x1d, 0xb8, 0x55, 0x7b, 0xd1, 0x54, 0x1b, 0xdf, 0x57, 0x8e, 0xda, 0x0d, 0xf5, 0x71, 0x4b, + 0x29, 0xab, 0x47, 0x4f, 0x9e, 0x2a, 0x2f, 0x42, 0x08, 0x64, 0xb8, 0x31, 0x5b, 0xac, 0xa1, 0x3e, + 0xee, 0xd4, 0xcb, 0x35, 0x45, 0x14, 0x8a, 0x3f, 0x86, 0x15, 0x7f, 0x87, 0x21, 0x61, 0x61, 0x72, + 0x35, 0xa5, 0xfd, 0xa4, 0x51, 0xe9, 0x28, 0x9f, 0x3e, 0x2b, 0x9f, 0xb4, 0xc4, 0x2b, 0x68, 0x0f, + 0xa4, 0x00, 0xa3, 0xd5, 0x2e, 0xab, 0xed, 0x56, 0xe7, 0x07, 0xd5, 0xf6, 0x13, 0x51, 0x20, 0x45, + 0x1c, 0xe0, 0x1e, 0x35, 0xea, 0xed, 0x72, 0xb5, 0xde, 0x12, 0x13, 0x87, 0xbf, 0x91, 0x20, 0x47, + 0xbe, 0x1d, 0x2d, 0x6c, 0x9f, 0x1b, 0x5d, 0x8c, 0x9e, 0xc2, 0xf2, 0x13, 0xac, 0xf5, 0xdd, 0xb3, + 0x2f, 0xd1, 0x66, 0xe4, 0x76, 0xa6, 0x0c, 0x2c, 0x77, 0xbc, 0x13, 0x43, 0x97, 0xc5, 0x37, 0x7f, + 0xfb, 0xe7, 0x2f, 0x12, 0x80, 0x32, 0xa5, 0x33, 0x6e, 0xe1, 0x31, 0x2c, 0xa9, 0x58, 0xd3, 0xc7, + 0x97, 0x36, 0x95, 0xa7, 0xa6, 0x32, 0x28, 0x5d, 0xb2, 0xa9, 0x7e, 0x1d, 0x32, 0xcf, 0xf9, 0xe8, + 0x3b, 0xd6, 0xd6, 0x56, 0x84, 0xde, 0xa2, 0x53, 0x76, 0x79, 0x9d, 0x1a, 0xcb, 0xa1, 0xec, 0x64, + 0x7c, 0x8e, 0x7e, 0x02, 0xeb, 0x8f, 0xb1, 0xcb, 0x9e, 0x73, 0xde, 0x98, 0x3a, 0xd6, 0xf0, 0x9d, + 0x45, 0x46, 0xde, 0x8e, 0xfc, 0xde, 0x9b, 0x3f, 0x4a, 0x6b, 0xb0, 0x4a, 0x64, 0xf0, 0xd0, 0x35, + 0xba, 0x9a, 0x8b, 0x75, 0xea, 0x19, 0x21, 0xb1, 0x34, 0xc0, 0x25, 0x72, 0x29, 0xf0, 0x86, 0xe9, + 0xe8, 0x4b, 0x10, 0x27, 0x00, 0xbc, 0xf1, 0x51, 0x9c, 0xff, 0x42, 0xac, 0x7f, 0xae, 0x29, 0xdf, + 0x8b, 0x73, 0xbd, 0x81, 0xd6, 0x99, 0x5f, 0x02, 0xc0, 0xe2, 0x7e, 0x7e, 0x29, 0xc0, 0x06, 0xbb, + 0x4d, 0x07, 0xfd, 0xdf, 0x9f, 0xed, 0x27, 0x66, 0x52, 0xb2, 0x00, 0xac, 0x52, 0x1c, 0xac, 0xcd, + 0x9d, 0x28, 0xac, 0x87, 0x42, 0x11, 0xb9, 0x90, 0x9f, 0x44, 0x85, 0x0d, 0x19, 0xe2, 0x62, 0x12, + 0x3f, 0x53, 0xa7, 0x7a, 0x72, 0x31, 0xce, 0xf5, 0x3a, 0x5a, 0x9b, 0xba, 0x66, 0x23, 0x8a, 0xaf, + 0x05, 0x58, 0x67, 0x37, 0x63, 0xbf, 0xe7, 0xf7, 0xe6, 0x44, 0xc3, 0xff, 0x02, 0x9f, 0x0b, 0xe7, + 0x7e, 0x1c, 0x9c, 0xab, 0x3b, 0x61, 0x38, 0x24, 0x0e, 0x3f, 0x17, 0x60, 0x3d, 0x32, 0x71, 0x88, + 0xc9, 0x4f, 0xdc, 0x64, 0x22, 0xf6, 0x6c, 0x7d, 0x27, 0x0e, 0xcb, 0x9e, 0xbc, 0x15, 0xc2, 0x52, + 0x62, 0x0f, 0xff, 0x31, 0xc1, 0xf4, 0x8d, 0x00, 0xd7, 0x55, 0xec, 0xe0, 0xa1, 0x5e, 0x1b, 0xfb, + 0x06, 0x37, 0x5d, 0xfa, 0xc4, 0xab, 0x5d, 0x94, 0xab, 0x38, 0x20, 0xe5, 0x38, 0x20, 0xfb, 0xf2, + 0xed, 0x08, 0x10, 0x9b, 0xba, 0x3e, 0xf7, 0xf9, 0x0c, 0x17, 0x0c, 0x9b, 0x6d, 0x5c, 0xbe, 0x60, + 0xa8, 0xde, 0x82, 0x05, 0xc3, 0x26, 0x23, 0xe1, 0x82, 0x61, 0x9e, 0xe7, 0x15, 0x8c, 0x7f, 0x82, + 0x30, 0x17, 0xce, 0x62, 0x05, 0x43, 0xe1, 0x90, 0x38, 0x7c, 0x1d, 0x2a, 0x98, 0x8b, 0x10, 0xcd, + 0x9e, 0x69, 0xbc, 0x65, 0xb9, 0x50, 0x24, 0x71, 0xe5, 0xe2, 0x9b, 0x15, 0xb1, 0xd4, 0xb1, 0x69, + 0xc5, 0xff, 0xa4, 0x5c, 0x38, 0x90, 0xd9, 0xe5, 0xe2, 0xef, 0xba, 0xde, 0xd4, 0xe3, 0xf2, 0x5d, + 0xd7, 0x7b, 0xb0, 0x2f, 0xd6, 0x75, 0xf9, 0xeb, 0x3f, 0xd2, 0x75, 0x3d, 0xff, 0xf3, 0xba, 0x6e, + 0x70, 0xda, 0xb1, 0x00, 0xac, 0xc5, 0xba, 0x2e, 0x87, 0x45, 0xa2, 0xf2, 0x0a, 0xb2, 0x34, 0x2a, + 0xb5, 0x53, 0x2d, 0x3e, 0x1c, 0xb7, 0xe6, 0xbd, 0xca, 0x1d, 0xf9, 0x6e, 0x9c, 0x63, 0x11, 0xe5, + 0xa7, 0x8e, 0xc9, 0xdb, 0x1d, 0xfd, 0x4c, 0x00, 0xd1, 0x3b, 0x41, 0x93, 0x41, 0xc3, 0xed, 0x99, + 0x2e, 0x82, 0xb3, 0x8b, 0xd8, 0xea, 0x78, 0x10, 0xe7, 0xbc, 0xb0, 0xb3, 0xeb, 0xab, 0x0e, 0x6e, + 0xcc, 0x29, 0xf1, 0xff, 0x06, 0x22, 0xfb, 0x1f, 0x43, 0xb6, 0xac, 0xeb, 0xe4, 0x51, 0xdf, 0x6e, + 0xc6, 0xee, 0xff, 0x76, 0xdc, 0x40, 0xc1, 0x37, 0x09, 0xb8, 0x20, 0xf4, 0xf2, 0x7a, 0x20, 0x02, + 0x25, 0xd3, 0xb5, 0x88, 0xeb, 0x37, 0x82, 0x7f, 0x5c, 0xd2, 0x6e, 0xa2, 0x5b, 0x17, 0xf5, 0x78, + 0xea, 0x2c, 0x36, 0x02, 0xdf, 0x8d, 0x73, 0x7e, 0x63, 0x67, 0x3b, 0xe2, 0xdc, 0x7f, 0x54, 0xfb, + 0xb0, 0xa2, 0xe2, 0x81, 0x79, 0x8e, 0xe7, 0x84, 0x20, 0xce, 0x71, 0xfc, 0x39, 0x28, 0x46, 0x77, + 0x8d, 0x7e, 0x27, 0xc0, 0x06, 0xbf, 0xd3, 0xfa, 0x6e, 0xcf, 0x0e, 0x3a, 0x58, 0xf0, 0xa9, 0xee, + 0x1d, 0x84, 0xd2, 0xc2, 0xf2, 0x3c, 0x39, 0xf1, 0x15, 0x22, 0xef, 0x96, 0x7a, 0x7d, 0xf3, 0xa5, + 0xd6, 0x27, 0x77, 0x11, 0xa2, 0x6c, 0xda, 0x3d, 0xa7, 0xd4, 0x71, 0xa8, 0x3e, 0xeb, 0x1b, 0x50, + 0x75, 0xaa, 0xda, 0x80, 0x3e, 0x7c, 0x63, 0xe3, 0x33, 0xfb, 0x4f, 0x23, 0x42, 0x2f, 0x6a, 0xf9, + 0x20, 0x0e, 0xc6, 0x35, 0xb4, 0xe1, 0xc1, 0xe8, 0x18, 0x8e, 0xa1, 0x0d, 0xe8, 0xab, 0x1b, 0xfd, + 0x54, 0x80, 0x2d, 0x7a, 0x3c, 0x3f, 0x63, 0xc6, 0xfd, 0xcf, 0xe8, 0x38, 0x24, 0x72, 0x5c, 0x6c, + 0xa6, 0xba, 0xf2, 0x61, 0x1c, 0x8e, 0x6d, 0xb4, 0x55, 0xf2, 0xbd, 0xc4, 0x4b, 0xdc, 0x54, 0x69, + 0x80, 0x1f, 0xfd, 0x59, 0xf8, 0xa6, 0xfc, 0x95, 0x80, 0x3e, 0x81, 0x0c, 0x79, 0x32, 0x14, 0xca, + 0xcd, 0xaa, 0x5c, 0x44, 0xfb, 0x67, 0xae, 0x6b, 0x39, 0x0f, 0x4b, 0xa5, 0x9e, 0xe1, 0x9e, 0x8d, + 0x5e, 0x1e, 0x74, 0xcd, 0x41, 0xa9, 0xab, 0x99, 0x53, 0x45, 0xeb, 0x8b, 0x5e, 0x89, 0xb8, 0x39, + 0x4c, 0xbe, 0x7f, 0xf0, 0x41, 0x51, 0x48, 0x1c, 0x8a, 0x9a, 0x65, 0xf5, 0x79, 0x5b, 0x2e, 0x7d, + 0xee, 0x98, 0xc3, 0x20, 0xa5, 0x67, 0x5b, 0xdd, 0x87, 0x11, 0x99, 0x87, 0x11, 0x99, 0xcf, 0xee, + 0xce, 0xf3, 0x48, 0xff, 0x56, 0x87, 0x88, 0xbe, 0x4c, 0xd3, 0x08, 0x7d, 0xf8, 0xdf, 0x00, 0x00, + 0x00, 0xff, 0xff, 0x64, 0x6c, 0xef, 0xe7, 0xe6, 0x23, 0x00, 0x00, } // Reference imports to suppress errors if they are not otherwise used. var _ context.Context -var _ grpc.ClientConn +var _ grpc.ClientConnInterface // This is a compile-time assertion to ensure that this generated file // is compatible with the grpc package it is being compiled against. -const _ = grpc.SupportPackageIsVersion4 +const _ = grpc.SupportPackageIsVersion6 // AuthServiceClient is the client API for AuthService service. // @@ -2204,11 +2269,11 @@ type AuthServiceClient interface { GetMyUserAddress(ctx context.Context, in *empty.Empty, opts ...grpc.CallOption) (*UserAddress, error) UpdateMyUserAddress(ctx context.Context, in *UpdateUserAddressRequest, opts ...grpc.CallOption) (*UserAddress, error) GetMyMfas(ctx context.Context, in *empty.Empty, opts ...grpc.CallOption) (*MultiFactors, error) - SetMyPassword(ctx context.Context, in *PasswordRequest, opts ...grpc.CallOption) (*empty.Empty, error) + //Password ChangeMyPassword(ctx context.Context, in *PasswordChange, opts ...grpc.CallOption) (*empty.Empty, error) // MFA AddMfaOTP(ctx context.Context, in *empty.Empty, opts ...grpc.CallOption) (*MfaOtpResponse, error) - VerifyMfaOTP(ctx context.Context, in *VerifyMfaOtp, opts ...grpc.CallOption) (*MfaOtpResponse, error) + VerifyMfaOTP(ctx context.Context, in *VerifyMfaOtp, opts ...grpc.CallOption) (*empty.Empty, error) RemoveMfaOTP(ctx context.Context, in *empty.Empty, opts ...grpc.CallOption) (*empty.Empty, error) SearchMyProjectOrgs(ctx context.Context, in *MyProjectOrgSearchRequest, opts ...grpc.CallOption) (*MyProjectOrgSearchResponse, error) IsIamAdmin(ctx context.Context, in *empty.Empty, opts ...grpc.CallOption) (*IsAdminResponse, error) @@ -2217,10 +2282,10 @@ type AuthServiceClient interface { } type authServiceClient struct { - cc *grpc.ClientConn + cc grpc.ClientConnInterface } -func NewAuthServiceClient(cc *grpc.ClientConn) AuthServiceClient { +func NewAuthServiceClient(cc grpc.ClientConnInterface) AuthServiceClient { return &authServiceClient{cc} } @@ -2377,15 +2442,6 @@ func (c *authServiceClient) GetMyMfas(ctx context.Context, in *empty.Empty, opts return out, nil } -func (c *authServiceClient) SetMyPassword(ctx context.Context, in *PasswordRequest, opts ...grpc.CallOption) (*empty.Empty, error) { - out := new(empty.Empty) - err := c.cc.Invoke(ctx, "/zitadel.auth.api.v1.AuthService/SetMyPassword", in, out, opts...) - if err != nil { - return nil, err - } - return out, nil -} - func (c *authServiceClient) ChangeMyPassword(ctx context.Context, in *PasswordChange, opts ...grpc.CallOption) (*empty.Empty, error) { out := new(empty.Empty) err := c.cc.Invoke(ctx, "/zitadel.auth.api.v1.AuthService/ChangeMyPassword", in, out, opts...) @@ -2404,8 +2460,8 @@ func (c *authServiceClient) AddMfaOTP(ctx context.Context, in *empty.Empty, opts return out, nil } -func (c *authServiceClient) VerifyMfaOTP(ctx context.Context, in *VerifyMfaOtp, opts ...grpc.CallOption) (*MfaOtpResponse, error) { - out := new(MfaOtpResponse) +func (c *authServiceClient) VerifyMfaOTP(ctx context.Context, in *VerifyMfaOtp, opts ...grpc.CallOption) (*empty.Empty, error) { + out := new(empty.Empty) err := c.cc.Invoke(ctx, "/zitadel.auth.api.v1.AuthService/VerifyMfaOTP", in, out, opts...) if err != nil { return nil, err @@ -2471,11 +2527,11 @@ type AuthServiceServer interface { GetMyUserAddress(context.Context, *empty.Empty) (*UserAddress, error) UpdateMyUserAddress(context.Context, *UpdateUserAddressRequest) (*UserAddress, error) GetMyMfas(context.Context, *empty.Empty) (*MultiFactors, error) - SetMyPassword(context.Context, *PasswordRequest) (*empty.Empty, error) + //Password ChangeMyPassword(context.Context, *PasswordChange) (*empty.Empty, error) // MFA AddMfaOTP(context.Context, *empty.Empty) (*MfaOtpResponse, error) - VerifyMfaOTP(context.Context, *VerifyMfaOtp) (*MfaOtpResponse, error) + VerifyMfaOTP(context.Context, *VerifyMfaOtp) (*empty.Empty, error) RemoveMfaOTP(context.Context, *empty.Empty) (*empty.Empty, error) SearchMyProjectOrgs(context.Context, *MyProjectOrgSearchRequest) (*MyProjectOrgSearchResponse, error) IsIamAdmin(context.Context, *empty.Empty) (*IsAdminResponse, error) @@ -2538,16 +2594,13 @@ func (*UnimplementedAuthServiceServer) UpdateMyUserAddress(ctx context.Context, func (*UnimplementedAuthServiceServer) GetMyMfas(ctx context.Context, req *empty.Empty) (*MultiFactors, error) { return nil, status.Errorf(codes.Unimplemented, "method GetMyMfas not implemented") } -func (*UnimplementedAuthServiceServer) SetMyPassword(ctx context.Context, req *PasswordRequest) (*empty.Empty, error) { - return nil, status.Errorf(codes.Unimplemented, "method SetMyPassword not implemented") -} func (*UnimplementedAuthServiceServer) ChangeMyPassword(ctx context.Context, req *PasswordChange) (*empty.Empty, error) { return nil, status.Errorf(codes.Unimplemented, "method ChangeMyPassword not implemented") } func (*UnimplementedAuthServiceServer) AddMfaOTP(ctx context.Context, req *empty.Empty) (*MfaOtpResponse, error) { return nil, status.Errorf(codes.Unimplemented, "method AddMfaOTP not implemented") } -func (*UnimplementedAuthServiceServer) VerifyMfaOTP(ctx context.Context, req *VerifyMfaOtp) (*MfaOtpResponse, error) { +func (*UnimplementedAuthServiceServer) VerifyMfaOTP(ctx context.Context, req *VerifyMfaOtp) (*empty.Empty, error) { return nil, status.Errorf(codes.Unimplemented, "method VerifyMfaOTP not implemented") } func (*UnimplementedAuthServiceServer) RemoveMfaOTP(ctx context.Context, req *empty.Empty) (*empty.Empty, error) { @@ -2873,24 +2926,6 @@ func _AuthService_GetMyMfas_Handler(srv interface{}, ctx context.Context, dec fu return interceptor(ctx, in, info, handler) } -func _AuthService_SetMyPassword_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { - in := new(PasswordRequest) - if err := dec(in); err != nil { - return nil, err - } - if interceptor == nil { - return srv.(AuthServiceServer).SetMyPassword(ctx, in) - } - info := &grpc.UnaryServerInfo{ - Server: srv, - FullMethod: "/zitadel.auth.api.v1.AuthService/SetMyPassword", - } - handler := func(ctx context.Context, req interface{}) (interface{}, error) { - return srv.(AuthServiceServer).SetMyPassword(ctx, req.(*PasswordRequest)) - } - return interceptor(ctx, in, info, handler) -} - func _AuthService_ChangeMyPassword_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { in := new(PasswordChange) if err := dec(in); err != nil { @@ -3089,10 +3124,6 @@ var _AuthService_serviceDesc = grpc.ServiceDesc{ MethodName: "GetMyMfas", Handler: _AuthService_GetMyMfas_Handler, }, - { - MethodName: "SetMyPassword", - Handler: _AuthService_SetMyPassword_Handler, - }, { MethodName: "ChangeMyPassword", Handler: _AuthService_ChangeMyPassword_Handler, diff --git a/pkg/auth/api/grpc/auth.pb.gw.go b/pkg/auth/api/grpc/auth.pb.gw.go index 960b322a88..b5ff7ac015 100644 --- a/pkg/auth/api/grpc/auth.pb.gw.go +++ b/pkg/auth/api/grpc/auth.pb.gw.go @@ -13,7 +13,6 @@ import ( "io" "net/http" - "github.com/golang/protobuf/descriptor" "github.com/golang/protobuf/proto" "github.com/golang/protobuf/ptypes/empty" "github.com/grpc-ecosystem/grpc-gateway/runtime" @@ -24,13 +23,11 @@ import ( "google.golang.org/grpc/status" ) -// Suppress "imported and not used" errors var _ codes.Code var _ io.Reader var _ status.Status var _ = runtime.String var _ = utilities.NewDoubleArray -var _ = descriptor.ForMessage func request_AuthService_Healthz_0(ctx context.Context, marshaler runtime.Marshaler, client AuthServiceClient, req *http.Request, pathParams map[string]string) (proto.Message, runtime.ServerMetadata, error) { var protoReq empty.Empty @@ -466,40 +463,6 @@ func local_request_AuthService_GetMyMfas_0(ctx context.Context, marshaler runtim } -func request_AuthService_SetMyPassword_0(ctx context.Context, marshaler runtime.Marshaler, client AuthServiceClient, req *http.Request, pathParams map[string]string) (proto.Message, runtime.ServerMetadata, error) { - var protoReq PasswordRequest - var metadata runtime.ServerMetadata - - newReader, berr := utilities.IOReaderFactory(req.Body) - if berr != nil { - return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", berr) - } - if err := marshaler.NewDecoder(newReader()).Decode(&protoReq); err != nil && err != io.EOF { - return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", err) - } - - msg, err := client.SetMyPassword(ctx, &protoReq, grpc.Header(&metadata.HeaderMD), grpc.Trailer(&metadata.TrailerMD)) - return msg, metadata, err - -} - -func local_request_AuthService_SetMyPassword_0(ctx context.Context, marshaler runtime.Marshaler, server AuthServiceServer, req *http.Request, pathParams map[string]string) (proto.Message, runtime.ServerMetadata, error) { - var protoReq PasswordRequest - var metadata runtime.ServerMetadata - - newReader, berr := utilities.IOReaderFactory(req.Body) - if berr != nil { - return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", berr) - } - if err := marshaler.NewDecoder(newReader()).Decode(&protoReq); err != nil && err != io.EOF { - return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", err) - } - - msg, err := server.SetMyPassword(ctx, &protoReq) - return msg, metadata, err - -} - func request_AuthService_ChangeMyPassword_0(ctx context.Context, marshaler runtime.Marshaler, client AuthServiceClient, req *http.Request, pathParams map[string]string) (proto.Message, runtime.ServerMetadata, error) { var protoReq PasswordChange var metadata runtime.ServerMetadata @@ -693,13 +656,13 @@ func local_request_AuthService_GetMyZitadelPermissions_0(ctx context.Context, ma // RegisterAuthServiceHandlerServer registers the http handlers for service AuthService to "mux". // UnaryRPC :call AuthServiceServer directly. // StreamingRPC :currently unsupported pending https://github.com/grpc/grpc-go/issues/906. -func RegisterAuthServiceHandlerServer(ctx context.Context, mux *runtime.ServeMux, server AuthServiceServer) error { +func RegisterAuthServiceHandlerServer(ctx context.Context, mux *runtime.ServeMux, server AuthServiceServer, opts []grpc.DialOption) error { mux.Handle("GET", pattern_AuthService_Healthz_0, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) { ctx, cancel := context.WithCancel(req.Context()) defer cancel() inboundMarshaler, outboundMarshaler := runtime.MarshalerForRequest(mux, req) - rctx, err := runtime.AnnotateIncomingContext(ctx, mux, req) + rctx, err := runtime.AnnotateContext(ctx, mux, req) if err != nil { runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err) return @@ -719,7 +682,7 @@ func RegisterAuthServiceHandlerServer(ctx context.Context, mux *runtime.ServeMux ctx, cancel := context.WithCancel(req.Context()) defer cancel() inboundMarshaler, outboundMarshaler := runtime.MarshalerForRequest(mux, req) - rctx, err := runtime.AnnotateIncomingContext(ctx, mux, req) + rctx, err := runtime.AnnotateContext(ctx, mux, req) if err != nil { runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err) return @@ -739,7 +702,7 @@ func RegisterAuthServiceHandlerServer(ctx context.Context, mux *runtime.ServeMux ctx, cancel := context.WithCancel(req.Context()) defer cancel() inboundMarshaler, outboundMarshaler := runtime.MarshalerForRequest(mux, req) - rctx, err := runtime.AnnotateIncomingContext(ctx, mux, req) + rctx, err := runtime.AnnotateContext(ctx, mux, req) if err != nil { runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err) return @@ -759,7 +722,7 @@ func RegisterAuthServiceHandlerServer(ctx context.Context, mux *runtime.ServeMux ctx, cancel := context.WithCancel(req.Context()) defer cancel() inboundMarshaler, outboundMarshaler := runtime.MarshalerForRequest(mux, req) - rctx, err := runtime.AnnotateIncomingContext(ctx, mux, req) + rctx, err := runtime.AnnotateContext(ctx, mux, req) if err != nil { runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err) return @@ -779,7 +742,7 @@ func RegisterAuthServiceHandlerServer(ctx context.Context, mux *runtime.ServeMux ctx, cancel := context.WithCancel(req.Context()) defer cancel() inboundMarshaler, outboundMarshaler := runtime.MarshalerForRequest(mux, req) - rctx, err := runtime.AnnotateIncomingContext(ctx, mux, req) + rctx, err := runtime.AnnotateContext(ctx, mux, req) if err != nil { runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err) return @@ -799,7 +762,7 @@ func RegisterAuthServiceHandlerServer(ctx context.Context, mux *runtime.ServeMux ctx, cancel := context.WithCancel(req.Context()) defer cancel() inboundMarshaler, outboundMarshaler := runtime.MarshalerForRequest(mux, req) - rctx, err := runtime.AnnotateIncomingContext(ctx, mux, req) + rctx, err := runtime.AnnotateContext(ctx, mux, req) if err != nil { runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err) return @@ -819,7 +782,7 @@ func RegisterAuthServiceHandlerServer(ctx context.Context, mux *runtime.ServeMux ctx, cancel := context.WithCancel(req.Context()) defer cancel() inboundMarshaler, outboundMarshaler := runtime.MarshalerForRequest(mux, req) - rctx, err := runtime.AnnotateIncomingContext(ctx, mux, req) + rctx, err := runtime.AnnotateContext(ctx, mux, req) if err != nil { runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err) return @@ -839,7 +802,7 @@ func RegisterAuthServiceHandlerServer(ctx context.Context, mux *runtime.ServeMux ctx, cancel := context.WithCancel(req.Context()) defer cancel() inboundMarshaler, outboundMarshaler := runtime.MarshalerForRequest(mux, req) - rctx, err := runtime.AnnotateIncomingContext(ctx, mux, req) + rctx, err := runtime.AnnotateContext(ctx, mux, req) if err != nil { runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err) return @@ -859,7 +822,7 @@ func RegisterAuthServiceHandlerServer(ctx context.Context, mux *runtime.ServeMux ctx, cancel := context.WithCancel(req.Context()) defer cancel() inboundMarshaler, outboundMarshaler := runtime.MarshalerForRequest(mux, req) - rctx, err := runtime.AnnotateIncomingContext(ctx, mux, req) + rctx, err := runtime.AnnotateContext(ctx, mux, req) if err != nil { runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err) return @@ -879,7 +842,7 @@ func RegisterAuthServiceHandlerServer(ctx context.Context, mux *runtime.ServeMux ctx, cancel := context.WithCancel(req.Context()) defer cancel() inboundMarshaler, outboundMarshaler := runtime.MarshalerForRequest(mux, req) - rctx, err := runtime.AnnotateIncomingContext(ctx, mux, req) + rctx, err := runtime.AnnotateContext(ctx, mux, req) if err != nil { runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err) return @@ -899,7 +862,7 @@ func RegisterAuthServiceHandlerServer(ctx context.Context, mux *runtime.ServeMux ctx, cancel := context.WithCancel(req.Context()) defer cancel() inboundMarshaler, outboundMarshaler := runtime.MarshalerForRequest(mux, req) - rctx, err := runtime.AnnotateIncomingContext(ctx, mux, req) + rctx, err := runtime.AnnotateContext(ctx, mux, req) if err != nil { runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err) return @@ -919,7 +882,7 @@ func RegisterAuthServiceHandlerServer(ctx context.Context, mux *runtime.ServeMux ctx, cancel := context.WithCancel(req.Context()) defer cancel() inboundMarshaler, outboundMarshaler := runtime.MarshalerForRequest(mux, req) - rctx, err := runtime.AnnotateIncomingContext(ctx, mux, req) + rctx, err := runtime.AnnotateContext(ctx, mux, req) if err != nil { runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err) return @@ -939,7 +902,7 @@ func RegisterAuthServiceHandlerServer(ctx context.Context, mux *runtime.ServeMux ctx, cancel := context.WithCancel(req.Context()) defer cancel() inboundMarshaler, outboundMarshaler := runtime.MarshalerForRequest(mux, req) - rctx, err := runtime.AnnotateIncomingContext(ctx, mux, req) + rctx, err := runtime.AnnotateContext(ctx, mux, req) if err != nil { runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err) return @@ -959,7 +922,7 @@ func RegisterAuthServiceHandlerServer(ctx context.Context, mux *runtime.ServeMux ctx, cancel := context.WithCancel(req.Context()) defer cancel() inboundMarshaler, outboundMarshaler := runtime.MarshalerForRequest(mux, req) - rctx, err := runtime.AnnotateIncomingContext(ctx, mux, req) + rctx, err := runtime.AnnotateContext(ctx, mux, req) if err != nil { runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err) return @@ -979,7 +942,7 @@ func RegisterAuthServiceHandlerServer(ctx context.Context, mux *runtime.ServeMux ctx, cancel := context.WithCancel(req.Context()) defer cancel() inboundMarshaler, outboundMarshaler := runtime.MarshalerForRequest(mux, req) - rctx, err := runtime.AnnotateIncomingContext(ctx, mux, req) + rctx, err := runtime.AnnotateContext(ctx, mux, req) if err != nil { runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err) return @@ -999,7 +962,7 @@ func RegisterAuthServiceHandlerServer(ctx context.Context, mux *runtime.ServeMux ctx, cancel := context.WithCancel(req.Context()) defer cancel() inboundMarshaler, outboundMarshaler := runtime.MarshalerForRequest(mux, req) - rctx, err := runtime.AnnotateIncomingContext(ctx, mux, req) + rctx, err := runtime.AnnotateContext(ctx, mux, req) if err != nil { runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err) return @@ -1019,7 +982,7 @@ func RegisterAuthServiceHandlerServer(ctx context.Context, mux *runtime.ServeMux ctx, cancel := context.WithCancel(req.Context()) defer cancel() inboundMarshaler, outboundMarshaler := runtime.MarshalerForRequest(mux, req) - rctx, err := runtime.AnnotateIncomingContext(ctx, mux, req) + rctx, err := runtime.AnnotateContext(ctx, mux, req) if err != nil { runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err) return @@ -1035,31 +998,11 @@ func RegisterAuthServiceHandlerServer(ctx context.Context, mux *runtime.ServeMux }) - mux.Handle("PUT", pattern_AuthService_SetMyPassword_0, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) { - ctx, cancel := context.WithCancel(req.Context()) - defer cancel() - inboundMarshaler, outboundMarshaler := runtime.MarshalerForRequest(mux, req) - rctx, err := runtime.AnnotateIncomingContext(ctx, mux, req) - if err != nil { - runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err) - return - } - resp, md, err := local_request_AuthService_SetMyPassword_0(rctx, inboundMarshaler, server, req, pathParams) - ctx = runtime.NewServerMetadataContext(ctx, md) - if err != nil { - runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err) - return - } - - forward_AuthService_SetMyPassword_0(ctx, mux, outboundMarshaler, w, req, resp, mux.GetForwardResponseOptions()...) - - }) - mux.Handle("PUT", pattern_AuthService_ChangeMyPassword_0, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) { ctx, cancel := context.WithCancel(req.Context()) defer cancel() inboundMarshaler, outboundMarshaler := runtime.MarshalerForRequest(mux, req) - rctx, err := runtime.AnnotateIncomingContext(ctx, mux, req) + rctx, err := runtime.AnnotateContext(ctx, mux, req) if err != nil { runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err) return @@ -1079,7 +1022,7 @@ func RegisterAuthServiceHandlerServer(ctx context.Context, mux *runtime.ServeMux ctx, cancel := context.WithCancel(req.Context()) defer cancel() inboundMarshaler, outboundMarshaler := runtime.MarshalerForRequest(mux, req) - rctx, err := runtime.AnnotateIncomingContext(ctx, mux, req) + rctx, err := runtime.AnnotateContext(ctx, mux, req) if err != nil { runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err) return @@ -1099,7 +1042,7 @@ func RegisterAuthServiceHandlerServer(ctx context.Context, mux *runtime.ServeMux ctx, cancel := context.WithCancel(req.Context()) defer cancel() inboundMarshaler, outboundMarshaler := runtime.MarshalerForRequest(mux, req) - rctx, err := runtime.AnnotateIncomingContext(ctx, mux, req) + rctx, err := runtime.AnnotateContext(ctx, mux, req) if err != nil { runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err) return @@ -1119,7 +1062,7 @@ func RegisterAuthServiceHandlerServer(ctx context.Context, mux *runtime.ServeMux ctx, cancel := context.WithCancel(req.Context()) defer cancel() inboundMarshaler, outboundMarshaler := runtime.MarshalerForRequest(mux, req) - rctx, err := runtime.AnnotateIncomingContext(ctx, mux, req) + rctx, err := runtime.AnnotateContext(ctx, mux, req) if err != nil { runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err) return @@ -1139,7 +1082,7 @@ func RegisterAuthServiceHandlerServer(ctx context.Context, mux *runtime.ServeMux ctx, cancel := context.WithCancel(req.Context()) defer cancel() inboundMarshaler, outboundMarshaler := runtime.MarshalerForRequest(mux, req) - rctx, err := runtime.AnnotateIncomingContext(ctx, mux, req) + rctx, err := runtime.AnnotateContext(ctx, mux, req) if err != nil { runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err) return @@ -1159,7 +1102,7 @@ func RegisterAuthServiceHandlerServer(ctx context.Context, mux *runtime.ServeMux ctx, cancel := context.WithCancel(req.Context()) defer cancel() inboundMarshaler, outboundMarshaler := runtime.MarshalerForRequest(mux, req) - rctx, err := runtime.AnnotateIncomingContext(ctx, mux, req) + rctx, err := runtime.AnnotateContext(ctx, mux, req) if err != nil { runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err) return @@ -1179,7 +1122,7 @@ func RegisterAuthServiceHandlerServer(ctx context.Context, mux *runtime.ServeMux ctx, cancel := context.WithCancel(req.Context()) defer cancel() inboundMarshaler, outboundMarshaler := runtime.MarshalerForRequest(mux, req) - rctx, err := runtime.AnnotateIncomingContext(ctx, mux, req) + rctx, err := runtime.AnnotateContext(ctx, mux, req) if err != nil { runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err) return @@ -1576,26 +1519,6 @@ func RegisterAuthServiceHandlerClient(ctx context.Context, mux *runtime.ServeMux }) - mux.Handle("PUT", pattern_AuthService_SetMyPassword_0, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) { - ctx, cancel := context.WithCancel(req.Context()) - defer cancel() - inboundMarshaler, outboundMarshaler := runtime.MarshalerForRequest(mux, req) - rctx, err := runtime.AnnotateContext(ctx, mux, req) - if err != nil { - runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err) - return - } - resp, md, err := request_AuthService_SetMyPassword_0(rctx, inboundMarshaler, client, req, pathParams) - ctx = runtime.NewServerMetadataContext(ctx, md) - if err != nil { - runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err) - return - } - - forward_AuthService_SetMyPassword_0(ctx, mux, outboundMarshaler, w, req, resp, mux.GetForwardResponseOptions()...) - - }) - mux.Handle("PUT", pattern_AuthService_ChangeMyPassword_0, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) { ctx, cancel := context.WithCancel(req.Context()) defer cancel() @@ -1774,8 +1697,6 @@ var ( pattern_AuthService_GetMyMfas_0 = runtime.MustPattern(runtime.NewPattern(1, []int{2, 0, 2, 1, 2, 2}, []string{"users", "me", "mfas"}, "", runtime.AssumeColonVerbOpt(true))) - pattern_AuthService_SetMyPassword_0 = runtime.MustPattern(runtime.NewPattern(1, []int{2, 0, 2, 1, 2, 2}, []string{"users", "me", "passwords"}, "", runtime.AssumeColonVerbOpt(true))) - pattern_AuthService_ChangeMyPassword_0 = runtime.MustPattern(runtime.NewPattern(1, []int{2, 0, 2, 1, 2, 2, 2, 3}, []string{"users", "me", "passwords", "_change"}, "", runtime.AssumeColonVerbOpt(true))) pattern_AuthService_AddMfaOTP_0 = runtime.MustPattern(runtime.NewPattern(1, []int{2, 0, 2, 1, 2, 2, 2, 3}, []string{"users", "me", "mfa", "otp"}, "", runtime.AssumeColonVerbOpt(true))) @@ -1826,8 +1747,6 @@ var ( forward_AuthService_GetMyMfas_0 = runtime.ForwardResponseMessage - forward_AuthService_SetMyPassword_0 = runtime.ForwardResponseMessage - forward_AuthService_ChangeMyPassword_0 = runtime.ForwardResponseMessage forward_AuthService_AddMfaOTP_0 = runtime.ForwardResponseMessage diff --git a/pkg/auth/api/grpc/auth.swagger.json b/pkg/auth/api/grpc/auth.swagger.json index ee2c9874fa..df818781aa 100644 --- a/pkg/auth/api/grpc/auth.swagger.json +++ b/pkg/auth/api/grpc/auth.swagger.json @@ -308,7 +308,7 @@ "200": { "description": "A successful response.", "schema": { - "$ref": "#/definitions/v1MfaOtpResponse" + "properties": {} } } }, @@ -343,34 +343,9 @@ ] } }, - "/users/me/passwords": { - "put": { - "operationId": "SetMyPassword", - "responses": { - "200": { - "description": "A successful response.", - "schema": { - "properties": {} - } - } - }, - "parameters": [ - { - "name": "body", - "in": "body", - "required": true, - "schema": { - "$ref": "#/definitions/v1PasswordRequest" - } - } - ], - "tags": [ - "AuthService" - ] - } - }, "/users/me/passwords/_change": { "put": { + "summary": "Password", "operationId": "ChangeMyPassword", "responses": { "200": { @@ -732,14 +707,6 @@ } } }, - "v1PasswordRequest": { - "type": "object", - "properties": { - "password": { - "type": "string" - } - } - }, "v1SearchMethod": { "type": "string", "enum": [ @@ -832,6 +799,14 @@ "sequence": { "type": "string", "format": "uint64" + }, + "creation_date": { + "type": "string", + "format": "date-time" + }, + "change_date": { + "type": "string", + "format": "date-time" } } }, @@ -851,6 +826,14 @@ "sequence": { "type": "string", "format": "uint64" + }, + "creation_date": { + "type": "string", + "format": "date-time" + }, + "change_date": { + "type": "string", + "format": "date-time" } } }, @@ -870,6 +853,14 @@ "sequence": { "type": "string", "format": "uint64" + }, + "creation_date": { + "type": "string", + "format": "date-time" + }, + "change_date": { + "type": "string", + "format": "date-time" } } }, @@ -903,6 +894,14 @@ "sequence": { "type": "string", "format": "uint64" + }, + "creation_date": { + "type": "string", + "format": "date-time" + }, + "change_date": { + "type": "string", + "format": "date-time" } } }, diff --git a/pkg/auth/api/grpc/generate.go b/pkg/auth/api/grpc/generate.go index fab0aadaac..a18052f119 100644 --- a/pkg/auth/api/grpc/generate.go +++ b/pkg/auth/api/grpc/generate.go @@ -1,4 +1,4 @@ package grpc //go:generate protoc -I$GOPATH/src -I../proto -I$GOPATH/src/github.com/grpc-ecosystem/grpc-gateway -I$GOPATH/src/github.com/grpc-ecosystem/grpc-gateway/third_party/googleapis -I${GOPATH}/src/github.com/envoyproxy/protoc-gen-validate -I${GOPATH}/src/github.com/caos/zitadel/internal/protoc/protoc-gen-authoption --go_out=plugins=grpc:$GOPATH/src --grpc-gateway_out=logtostderr=true:$GOPATH/src --swagger_out=logtostderr=true:. --authoption_out=. ../proto/auth.proto -//go:generate mockgen -package api -destination ./mock/auth.proto.mock.go github.com/caos/zitadel/pkg/auth/api/grpc AuthServiceClient \ No newline at end of file +//go:generate mockgen -package api -destination ./mock/auth.proto.mock.go github.com/caos/zitadel/pkg/auth/api/grpc AuthServiceClient diff --git a/pkg/auth/api/grpc/server.go b/pkg/auth/api/grpc/server.go index 5af9cad064..7c55eaf303 100644 --- a/pkg/auth/api/grpc/server.go +++ b/pkg/auth/api/grpc/server.go @@ -4,20 +4,28 @@ import ( grpc_middleware "github.com/grpc-ecosystem/go-grpc-middleware" "google.golang.org/grpc" + auth_util "github.com/caos/zitadel/internal/api/auth" grpc_util "github.com/caos/zitadel/internal/api/grpc" "github.com/caos/zitadel/internal/api/grpc/server/middleware" + "github.com/caos/zitadel/internal/auth/auth" + "github.com/caos/zitadel/internal/auth/repository" ) var _ AuthServiceServer = (*Server)(nil) type Server struct { - port string - searchLimit int + port string + repo repository.Repository + verifier *auth.TokenVerifier + authZ auth_util.Config } -func StartServer(conf grpc_util.ServerConfig) *Server { +func StartServer(conf grpc_util.ServerConfig, authZ auth_util.Config, repo repository.Repository) *Server { return &Server{ - port: conf.Port, + port: conf.Port, + repo: repo, + authZ: authZ, + verifier: auth.Start(), } } @@ -31,6 +39,7 @@ func (s *Server) GRPCServer() (*grpc.Server, error) { grpc.UnaryInterceptor( grpc_middleware.ChainUnaryServer( middleware.ErrorHandler(), + AuthService_Authorization_Interceptor(s.verifier, &s.authZ), ), ), ) diff --git a/pkg/auth/api/grpc/user.go b/pkg/auth/api/grpc/user.go index bb4b5ff65b..5788c82df2 100644 --- a/pkg/auth/api/grpc/user.go +++ b/pkg/auth/api/grpc/user.go @@ -2,24 +2,42 @@ package grpc import ( "context" - "github.com/caos/zitadel/internal/errors" + "github.com/golang/protobuf/ptypes/empty" + + "github.com/caos/zitadel/internal/errors" ) func (s *Server) GetMyUserProfile(ctx context.Context, _ *empty.Empty) (*UserProfile, error) { - return nil, errors.ThrowUnimplemented(nil, "GRPC-fis93", "Not implemented") + profile, err := s.repo.MyProfile(ctx) + if err != nil { + return nil, err + } + return profileFromModel(profile), nil } func (s *Server) GetMyUserEmail(ctx context.Context, _ *empty.Empty) (*UserEmail, error) { - return nil, errors.ThrowUnimplemented(nil, "GRPC-93j5d", "Not implemented") + email, err := s.repo.MyEmail(ctx) + if err != nil { + return nil, err + } + return emailFromModel(email), nil } func (s *Server) GetMyUserPhone(ctx context.Context, _ *empty.Empty) (*UserPhone, error) { - return nil, errors.ThrowUnimplemented(nil, "GRPC-Hj75G", "Not implemented") + phone, err := s.repo.MyPhone(ctx) + if err != nil { + return nil, err + } + return phoneFromModel(phone), nil } func (s *Server) GetMyUserAddress(ctx context.Context, _ *empty.Empty) (*UserAddress, error) { - return nil, errors.ThrowUnimplemented(nil, "GRPC-21jd4", "Not implemented") + address, err := s.repo.MyAddress(ctx) + if err != nil { + return nil, err + } + return addressFromModel(address), nil } func (s *Server) GetMyMfas(ctx context.Context, _ *empty.Empty) (*MultiFactors, error) { @@ -27,57 +45,76 @@ func (s *Server) GetMyMfas(ctx context.Context, _ *empty.Empty) (*MultiFactors, } func (s *Server) UpdateMyUserProfile(ctx context.Context, request *UpdateUserProfileRequest) (*UserProfile, error) { - return nil, errors.ThrowUnimplemented(nil, "GRPC-dlep3", "Not implemented") + profile, err := s.repo.ChangeMyProfile(ctx, updateProfileToModel(ctx, request)) + if err != nil { + return nil, err + } + return profileFromModel(profile), nil } func (s *Server) ChangeMyUserEmail(ctx context.Context, request *UpdateUserEmailRequest) (*UserEmail, error) { - return nil, errors.ThrowUnimplemented(nil, "GRPC-lme45", "Not implemented") + email, err := s.repo.ChangeMyEmail(ctx, updateEmailToModel(ctx, request)) + if err != nil { + return nil, err + } + return emailFromModel(email), nil } func (s *Server) VerifyMyUserEmail(ctx context.Context, request *VerifyMyUserEmailRequest) (*empty.Empty, error) { - return nil, errors.ThrowUnimplemented(nil, "GRPC-poru7", "Not implemented") -} - -func (s *Server) VerifyUserEmail(ctx context.Context, request *VerifyUserEmailRequest) (*empty.Empty, error) { - return nil, errors.ThrowUnimplemented(nil, "GRPC-dlep3", "Not implemented") + err := s.repo.VerifyMyEmail(ctx, request.Code) + return &empty.Empty{}, err } func (s *Server) ResendMyEmailVerificationMail(ctx context.Context, _ *empty.Empty) (*empty.Empty, error) { - return nil, errors.ThrowUnimplemented(nil, "GRPC-dh69i", "Not implemented") + err := s.repo.ResendMyEmailVerificationMail(ctx) + return &empty.Empty{}, err } func (s *Server) ChangeMyUserPhone(ctx context.Context, request *UpdateUserPhoneRequest) (*UserPhone, error) { - return nil, errors.ThrowUnimplemented(nil, "GRPC-dk45g", "Not implemented") + phone, err := s.repo.ChangeMyPhone(ctx, updatePhoneToModel(ctx, request)) + if err != nil { + return nil, err + } + return phoneFromModel(phone), nil } func (s *Server) VerifyMyUserPhone(ctx context.Context, request *VerifyUserPhoneRequest) (*empty.Empty, error) { - return nil, errors.ThrowUnimplemented(nil, "GRPC-ol6gE", "Not implemented") + err := s.repo.VerifyMyPhone(ctx, request.Code) + return &empty.Empty{}, err } func (s *Server) ResendMyPhoneVerificationCode(ctx context.Context, _ *empty.Empty) (*empty.Empty, error) { - return nil, errors.ThrowUnimplemented(nil, "GRPC-Wk8rf", "Not implemented") + err := s.repo.ResendMyPhoneVerificationCode(ctx) + return &empty.Empty{}, err } func (s *Server) UpdateMyUserAddress(ctx context.Context, request *UpdateUserAddressRequest) (*UserAddress, error) { - return nil, errors.ThrowUnimplemented(nil, "GRPC-cmt7F", "Not implemented") -} - -func (s *Server) SetMyPassword(ctx context.Context, request *PasswordRequest) (*empty.Empty, error) { - return nil, errors.ThrowUnimplemented(nil, "GRPC-pl9c2", "Not implemented") + address, err := s.repo.ChangeMyAddress(ctx, updateAddressToModel(ctx, request)) + if err != nil { + return nil, err + } + return addressFromModel(address), nil } func (s *Server) ChangeMyPassword(ctx context.Context, request *PasswordChange) (*empty.Empty, error) { - return nil, errors.ThrowUnimplemented(nil, "GRPC-dlo6G", "Not implemented") + err := s.repo.ChangeMyPassword(ctx, request.OldPassword, request.NewPassword) + return &empty.Empty{}, err } func (s *Server) AddMfaOTP(ctx context.Context, _ *empty.Empty) (_ *MfaOtpResponse, err error) { - return nil, errors.ThrowUnimplemented(nil, "GRPC-al35G", "Not implemented") + otp, err := s.repo.AddMyMfaOTP(ctx) + if err != nil { + return nil, err + } + return otpFromModel(otp), nil } -func (s *Server) VerifyMfaOTP(ctx context.Context, request *VerifyMfaOtp) (_ *MfaOtpResponse, err error) { - return nil, errors.ThrowUnimplemented(nil, "GRPC-kgjZ7", "Not implemented") +func (s *Server) VerifyMfaOTP(ctx context.Context, request *VerifyMfaOtp) (*empty.Empty, error) { + err := s.repo.VerifyMyMfaOTP(ctx, request.Code) + return &empty.Empty{}, err } func (s *Server) RemoveMfaOTP(ctx context.Context, _ *empty.Empty) (_ *empty.Empty, err error) { - return nil, errors.ThrowUnimplemented(nil, "GRPC-9k46d", "Not implemented") + s.repo.RemoveMyMfaOTP(ctx) + return &empty.Empty{}, err } diff --git a/pkg/auth/api/grpc/user_converter.go b/pkg/auth/api/grpc/user_converter.go new file mode 100644 index 0000000000..bcb6946bf2 --- /dev/null +++ b/pkg/auth/api/grpc/user_converter.go @@ -0,0 +1,174 @@ +package grpc + +import ( + "context" + + "github.com/caos/logging" + "github.com/caos/zitadel/internal/api/auth" + "github.com/caos/zitadel/internal/eventstore/models" + usr_model "github.com/caos/zitadel/internal/user/model" + "github.com/golang/protobuf/ptypes" + "golang.org/x/text/language" +) + +func profileFromModel(profile *usr_model.Profile) *UserProfile { + creationDate, err := ptypes.TimestampProto(profile.CreationDate) + logging.Log("GRPC-56t5s").OnError(err).Debug("unable to parse timestamp") + + changeDate, err := ptypes.TimestampProto(profile.ChangeDate) + logging.Log("GRPC-K58ds").OnError(err).Debug("unable to parse timestamp") + + return &UserProfile{ + Id: profile.AggregateID, + CreationDate: creationDate, + ChangeDate: changeDate, + Sequence: profile.Sequence, + UserName: profile.UserName, + FirstName: profile.FirstName, + LastName: profile.LastName, + DisplayName: profile.DisplayName, + NickName: profile.NickName, + PreferredLanguage: profile.PreferredLanguage.String(), + Gender: genderFromModel(profile.Gender), + } +} + +func updateProfileToModel(ctx context.Context, u *UpdateUserProfileRequest) *usr_model.Profile { + preferredLanguage, err := language.Parse(u.PreferredLanguage) + logging.Log("GRPC-lk73L").OnError(err).Debug("language malformed") + + return &usr_model.Profile{ + ObjectRoot: models.ObjectRoot{AggregateID: auth.GetCtxData(ctx).UserID}, + FirstName: u.FirstName, + LastName: u.LastName, + NickName: u.NickName, + DisplayName: u.DisplayName, + PreferredLanguage: preferredLanguage, + Gender: genderToModel(u.Gender), + } +} + +func emailFromModel(email *usr_model.Email) *UserEmail { + creationDate, err := ptypes.TimestampProto(email.CreationDate) + logging.Log("GRPC-sdoi3").OnError(err).Debug("unable to parse timestamp") + + changeDate, err := ptypes.TimestampProto(email.ChangeDate) + logging.Log("GRPC-klJK3").OnError(err).Debug("unable to parse timestamp") + + return &UserEmail{ + Id: email.AggregateID, + CreationDate: creationDate, + ChangeDate: changeDate, + Sequence: email.Sequence, + Email: email.EmailAddress, + IsEmailVerified: email.IsEmailVerified, + } +} + +func updateEmailToModel(ctx context.Context, e *UpdateUserEmailRequest) *usr_model.Email { + return &usr_model.Email{ + ObjectRoot: models.ObjectRoot{AggregateID: auth.GetCtxData(ctx).UserID}, + EmailAddress: e.Email, + } +} + +func phoneFromModel(phone *usr_model.Phone) *UserPhone { + creationDate, err := ptypes.TimestampProto(phone.CreationDate) + logging.Log("GRPC-kjn5J").OnError(err).Debug("unable to parse timestamp") + + changeDate, err := ptypes.TimestampProto(phone.ChangeDate) + logging.Log("GRPC-LKA9S").OnError(err).Debug("unable to parse timestamp") + + return &UserPhone{ + Id: phone.AggregateID, + CreationDate: creationDate, + ChangeDate: changeDate, + Sequence: phone.Sequence, + Phone: phone.PhoneNumber, + IsPhoneVerified: phone.IsPhoneVerified, + } +} + +func updatePhoneToModel(ctx context.Context, e *UpdateUserPhoneRequest) *usr_model.Phone { + return &usr_model.Phone{ + ObjectRoot: models.ObjectRoot{AggregateID: auth.GetCtxData(ctx).UserID}, + PhoneNumber: e.Phone, + } +} + +func addressFromModel(address *usr_model.Address) *UserAddress { + creationDate, err := ptypes.TimestampProto(address.CreationDate) + logging.Log("GRPC-65FRs").OnError(err).Debug("unable to parse timestamp") + + changeDate, err := ptypes.TimestampProto(address.ChangeDate) + logging.Log("GRPC-aslk4").OnError(err).Debug("unable to parse timestamp") + + return &UserAddress{ + Id: address.AggregateID, + CreationDate: creationDate, + ChangeDate: changeDate, + Sequence: address.Sequence, + Country: address.Country, + StreetAddress: address.StreetAddress, + Region: address.Region, + PostalCode: address.PostalCode, + Locality: address.Locality, + } +} + +func updateAddressToModel(ctx context.Context, address *UpdateUserAddressRequest) *usr_model.Address { + return &usr_model.Address{ + ObjectRoot: models.ObjectRoot{AggregateID: auth.GetCtxData(ctx).UserID}, + Country: address.Country, + StreetAddress: address.StreetAddress, + Region: address.Region, + PostalCode: address.PostalCode, + Locality: address.Locality, + } +} + +func otpFromModel(otp *usr_model.OTP) *MfaOtpResponse { + return &MfaOtpResponse{ + UserId: otp.AggregateID, + Url: otp.Url, + Secret: otp.SecretString, + State: mfaStateFromModel(otp.State), + } +} + +func genderFromModel(gender usr_model.Gender) Gender { + switch gender { + case usr_model.GENDER_FEMALE: + return Gender_GENDER_FEMALE + case usr_model.GENDER_MALE: + return Gender_GENDER_MALE + case usr_model.GENDER_DIVERSE: + return Gender_GENDER_DIVERSE + default: + return Gender_GENDER_UNSPECIFIED + } +} + +func genderToModel(gender Gender) usr_model.Gender { + switch gender { + case Gender_GENDER_FEMALE: + return usr_model.GENDER_FEMALE + case Gender_GENDER_MALE: + return usr_model.GENDER_MALE + case Gender_GENDER_DIVERSE: + return usr_model.GENDER_DIVERSE + default: + return usr_model.GENDER_UNDEFINED + } +} + +func mfaStateFromModel(state usr_model.MfaState) MFAState { + switch state { + case usr_model.MFASTATE_READY: + return MFAState_MFASTATE_NOT_READY + case usr_model.MFASTATE_NOTREADY: + return MFAState_MFASTATE_NOT_READY + default: + return MFAState_MFASTATE_UNSPECIFIED + } +} diff --git a/pkg/auth/api/proto/auth.proto b/pkg/auth/api/proto/auth.proto index ba0a148349..89fd78fe33 100644 --- a/pkg/auth/api/proto/auth.proto +++ b/pkg/auth/api/proto/auth.proto @@ -13,515 +13,511 @@ package zitadel.auth.api.v1; option go_package = "github.com/caos/zitadel/pkg/auth/api/grpc"; option (grpc.gateway.protoc_gen_swagger.options.openapiv2_swagger) = { - info: { - title: "Auth API"; - version: "0.1"; - contact:{ - url: "https://github.com/caos/zitadel/pkg/auth" - }; + info: { + title: "Auth API"; + version: "0.1"; + contact:{ + url: "https://github.com/caos/zitadel/pkg/auth" }; + }; - schemes: HTTPS; + schemes: HTTPS; - consumes: "application/json"; - consumes: "application/grpc"; + consumes: "application/json"; + consumes: "application/grpc"; - produces: "application/json"; - produces: "application/grpc"; + produces: "application/json"; + produces: "application/grpc"; }; service AuthService { - // Readiness - rpc Healthz(google.protobuf.Empty) returns (google.protobuf.Empty) { - option (google.api.http) = { - get: "/healthz" - }; - } + // Readiness + rpc Healthz(google.protobuf.Empty) returns (google.protobuf.Empty) { + option (google.api.http) = { + get: "/healthz" + }; + } - rpc Ready(google.protobuf.Empty) returns (google.protobuf.Empty) { - option (google.api.http) = { - get: "/ready" - }; - } + rpc Ready(google.protobuf.Empty) returns (google.protobuf.Empty) { + option (google.api.http) = { + get: "/ready" + }; + } - rpc Validate(google.protobuf.Empty) returns (google.protobuf.Struct) { - option (google.api.http) = { - get: "/validate" - }; - } + rpc Validate(google.protobuf.Empty) returns (google.protobuf.Struct) { + option (google.api.http) = { + get: "/validate" + }; + } - // Authorization - rpc GetMyUserSessions(google.protobuf.Empty) returns (UserSessionViews) { - option (google.api.http) = { - get: "/me/usersessions" - }; + // Authorization + rpc GetMyUserSessions(google.protobuf.Empty) returns (UserSessionViews) { + option (google.api.http) = { + get: "/me/usersessions" + }; - option (caos.zitadel.utils.v1.auth_option) = { - permission: "authenticated" - }; - } + option (caos.zitadel.utils.v1.auth_option) = { + permission: "authenticated" + }; + } - //User - rpc GetMyUserProfile(google.protobuf.Empty) returns (UserProfile) { - option (google.api.http) = { - get: "/users/me/profile" - }; + //User + rpc GetMyUserProfile(google.protobuf.Empty) returns (UserProfile) { + option (google.api.http) = { + get: "/users/me/profile" + }; - option (caos.zitadel.utils.v1.auth_option) = { - permission: "authenticated" - }; - } + option (caos.zitadel.utils.v1.auth_option) = { + permission: "authenticated" + }; + } - rpc UpdateMyUserProfile(UpdateUserProfileRequest) returns (UserProfile) { - option (google.api.http) = { - put: "/users/me/profile" - body: "*" - }; + rpc UpdateMyUserProfile(UpdateUserProfileRequest) returns (UserProfile) { + option (google.api.http) = { + put: "/users/me/profile" + body: "*" + }; - option (caos.zitadel.utils.v1.auth_option) = { - permission: "authenticated" - }; - } + option (caos.zitadel.utils.v1.auth_option) = { + permission: "authenticated" + }; + } - rpc GetMyUserEmail(google.protobuf.Empty) returns (UserEmail) { - option (google.api.http) = { - get: "/users/me/email" - }; + rpc GetMyUserEmail(google.protobuf.Empty) returns (UserEmail) { + option (google.api.http) = { + get: "/users/me/email" + }; - option (caos.zitadel.utils.v1.auth_option) = { - permission: "authenticated" - }; - } + option (caos.zitadel.utils.v1.auth_option) = { + permission: "authenticated" + }; + } - rpc ChangeMyUserEmail(UpdateUserEmailRequest) returns (UserEmail) { - option (google.api.http) = { - put: "/users/me/email" - body: "*" - }; + rpc ChangeMyUserEmail(UpdateUserEmailRequest) returns (UserEmail) { + option (google.api.http) = { + put: "/users/me/email" + body: "*" + }; - option (caos.zitadel.utils.v1.auth_option) = { - permission: "authenticated" - }; - } + option (caos.zitadel.utils.v1.auth_option) = { + permission: "authenticated" + }; + } - rpc VerifyMyUserEmail(VerifyMyUserEmailRequest) returns (google.protobuf.Empty) { - option (google.api.http) = { - post: "/users/me/email/_verify" - body: "*" - }; + rpc VerifyMyUserEmail(VerifyMyUserEmailRequest) returns (google.protobuf.Empty) { + option (google.api.http) = { + post: "/users/me/email/_verify" + body: "*" + }; - option (caos.zitadel.utils.v1.auth_option) = { - permission: "authenticated" - }; - } + option (caos.zitadel.utils.v1.auth_option) = { + permission: "authenticated" + }; + } - rpc ResendMyEmailVerificationMail(google.protobuf.Empty) returns (google.protobuf.Empty) { - option (google.api.http) = { - post: "/users/me/email/_resendverification" - body: "*" - }; + rpc ResendMyEmailVerificationMail(google.protobuf.Empty) returns (google.protobuf.Empty) { + option (google.api.http) = { + post: "/users/me/email/_resendverification" + body: "*" + }; - option (caos.zitadel.utils.v1.auth_option) = { - permission: "authenticated" - }; - } + option (caos.zitadel.utils.v1.auth_option) = { + permission: "authenticated" + }; + } - rpc GetMyUserPhone(google.protobuf.Empty) returns (UserPhone) { - option (google.api.http) = { - get: "/users/me/phone" - }; + rpc GetMyUserPhone(google.protobuf.Empty) returns (UserPhone) { + option (google.api.http) = { + get: "/users/me/phone" + }; - option (caos.zitadel.utils.v1.auth_option) = { - permission: "authenticated" - }; - } + option (caos.zitadel.utils.v1.auth_option) = { + permission: "authenticated" + }; + } - rpc ChangeMyUserPhone(UpdateUserPhoneRequest) returns (UserPhone) { - option (google.api.http) = { - put: "/users/me/phone" - body: "*" - }; + rpc ChangeMyUserPhone(UpdateUserPhoneRequest) returns (UserPhone) { + option (google.api.http) = { + put: "/users/me/phone" + body: "*" + }; - option (caos.zitadel.utils.v1.auth_option) = { - permission: "authenticated" - }; - } + option (caos.zitadel.utils.v1.auth_option) = { + permission: "authenticated" + }; + } - rpc VerifyMyUserPhone(VerifyUserPhoneRequest) returns (google.protobuf.Empty) { - option (google.api.http) = { - post: "/users/me/phone/_verify" - body: "*" - }; + rpc VerifyMyUserPhone(VerifyUserPhoneRequest) returns (google.protobuf.Empty) { + option (google.api.http) = { + post: "/users/me/phone/_verify" + body: "*" + }; - option (caos.zitadel.utils.v1.auth_option) = { - permission: "authenticated" - }; - } + option (caos.zitadel.utils.v1.auth_option) = { + permission: "authenticated" + }; + } - rpc ResendMyPhoneVerificationCode(google.protobuf.Empty) returns (google.protobuf.Empty) { - option (google.api.http) = { - post: "/users/me/phone/_resendverification" - body: "*" - }; + rpc ResendMyPhoneVerificationCode(google.protobuf.Empty) returns (google.protobuf.Empty) { + option (google.api.http) = { + post: "/users/me/phone/_resendverification" + body: "*" + }; - option (caos.zitadel.utils.v1.auth_option) = { - permission: "authenticated" - }; - } + option (caos.zitadel.utils.v1.auth_option) = { + permission: "authenticated" + }; + } - rpc GetMyUserAddress(google.protobuf.Empty) returns (UserAddress) { - option (google.api.http) = { - get: "/users/me/address" - }; + rpc GetMyUserAddress(google.protobuf.Empty) returns (UserAddress) { + option (google.api.http) = { + get: "/users/me/address" + }; - option (caos.zitadel.utils.v1.auth_option) = { - permission: "authenticated" - }; - } + option (caos.zitadel.utils.v1.auth_option) = { + permission: "authenticated" + }; + } - rpc UpdateMyUserAddress(UpdateUserAddressRequest) returns (UserAddress) { - option (google.api.http) = { - put: "/users/me/address" - body: "*" - }; + rpc UpdateMyUserAddress(UpdateUserAddressRequest) returns (UserAddress) { + option (google.api.http) = { + put: "/users/me/address" + body: "*" + }; - option (caos.zitadel.utils.v1.auth_option) = { - permission: "authenticated" - }; - } + option (caos.zitadel.utils.v1.auth_option) = { + permission: "authenticated" + }; + } - rpc GetMyMfas(google.protobuf.Empty) returns (MultiFactors) { - option (google.api.http) = { - get: "/users/me/mfas" - }; + rpc GetMyMfas(google.protobuf.Empty) returns (MultiFactors) { + option (google.api.http) = { + get: "/users/me/mfas" + }; - option (caos.zitadel.utils.v1.auth_option) = { - permission: "authenticated" - }; - } - //Password + option (caos.zitadel.utils.v1.auth_option) = { + permission: "authenticated" + }; + } + //Password + rpc ChangeMyPassword(PasswordChange) returns (google.protobuf.Empty) { + option (google.api.http) = { + put: "/users/me/passwords/_change" + body: "*" + }; - rpc SetMyPassword(PasswordRequest) returns (google.protobuf.Empty) { - option (google.api.http) = { - put: "/users/me/passwords" - body: "*" - }; + option (caos.zitadel.utils.v1.auth_option) = { + permission: "authenticated" + }; + } - option (caos.zitadel.utils.v1.auth_option) = { - permission: "authenticated" - }; - } + // MFA + rpc AddMfaOTP(google.protobuf.Empty) returns (MfaOtpResponse) { + option (google.api.http) = { + post: "/users/me/mfa/otp" + body: "*" + }; + option (caos.zitadel.utils.v1.auth_option) = { + permission: "authenticated" + }; + } - rpc ChangeMyPassword(PasswordChange) returns (google.protobuf.Empty) { - option (google.api.http) = { - put: "/users/me/passwords/_change" - body: "*" - }; + rpc VerifyMfaOTP(VerifyMfaOtp) returns (google.protobuf.Empty) { + option (google.api.http) = { + put: "/users/me/mfa/otp/_verify" + body: "*" + }; - option (caos.zitadel.utils.v1.auth_option) = { - permission: "authenticated" - }; - } + option (caos.zitadel.utils.v1.auth_option) = { + permission: "authenticated" + }; + } - // MFA - rpc AddMfaOTP(google.protobuf.Empty) returns (MfaOtpResponse) { - option (google.api.http) = { - post: "/users/me/mfa/otp" - body: "*" - }; - option (caos.zitadel.utils.v1.auth_option) = { - permission: "authenticated" - }; - } + rpc RemoveMfaOTP(google.protobuf.Empty) returns (google.protobuf.Empty) { + option (google.api.http) = { + delete: "/users/me/mfa/otp" + }; - rpc VerifyMfaOTP(VerifyMfaOtp) returns (MfaOtpResponse) { - option (google.api.http) = { - put: "/users/me/mfa/otp/_verify" - body: "*" - }; + option (caos.zitadel.utils.v1.auth_option) = { + permission: "authenticated" + }; + } - option (caos.zitadel.utils.v1.auth_option) = { - permission: "authenticated" - }; - } + rpc SearchMyProjectOrgs(MyProjectOrgSearchRequest) returns (MyProjectOrgSearchResponse) { + option (google.api.http) = { + post: "/global/projectorgs/_search" + body: "*" + }; - rpc RemoveMfaOTP(google.protobuf.Empty) returns (google.protobuf.Empty) { - option (google.api.http) = { - delete: "/users/me/mfa/otp" - }; + option (caos.zitadel.utils.v1.auth_option) = { + permission: "authenticated" + }; + } - option (caos.zitadel.utils.v1.auth_option) = { - permission: "authenticated" - }; - } + rpc IsIamAdmin(google.protobuf.Empty) returns (IsAdminResponse) { + option (google.api.http) = { + get: "/global/_isiamadmin" + }; - rpc SearchMyProjectOrgs(MyProjectOrgSearchRequest) returns (MyProjectOrgSearchResponse) { - option (google.api.http) = { - post: "/global/projectorgs/_search" - body: "*" - }; + option (caos.zitadel.utils.v1.auth_option) = { + permission: "authenticated" + }; + } - option (caos.zitadel.utils.v1.auth_option) = { - permission: "authenticated" - }; - } + //Permission + rpc GetMyZitadelPermissions(google.protobuf.Empty) returns (MyPermissions) { + option (google.api.http) = { + get: "/permissions/zitadel/me" + }; - rpc IsIamAdmin(google.protobuf.Empty) returns (IsAdminResponse) { - option (google.api.http) = { - get: "/global/_isiamadmin" - }; - - option (caos.zitadel.utils.v1.auth_option) = { - permission: "authenticated" - }; - } - - //Permission - rpc GetMyZitadelPermissions(google.protobuf.Empty) returns (MyPermissions) { - option (google.api.http) = { - get: "/permissions/zitadel/me" - }; - - option (caos.zitadel.utils.v1.auth_option) = { - permission: "authenticated" - }; - } + option (caos.zitadel.utils.v1.auth_option) = { + permission: "authenticated" + }; + } } message UserSessionViews { - repeated UserSessionView user_sessions = 1; + repeated UserSessionView user_sessions = 1; } message UserSessionView { - string id = 1; - string agent_id = 2; - UserSessionState auth_state = 3; - string user_id = 4; - string user_name = 5; - uint64 sequence = 6; + string id = 1; + string agent_id = 2; + UserSessionState auth_state = 3; + string user_id = 4; + string user_name = 5; + uint64 sequence = 6; } enum UserSessionState { - USERSESSIONSTATE_UNSPECIFIED = 0; - USERSESSIONSTATE_ACTIVE = 1; - USERSESSIONSTATE_TERMINATED = 2; + USERSESSIONSTATE_UNSPECIFIED = 0; + USERSESSIONSTATE_ACTIVE = 1; + USERSESSIONSTATE_TERMINATED = 2; } enum OIDCResponseType { - OIDCRESPONSETYPE_CODE = 0; - OIDCRESPONSETYPE_ID_TOKEN = 1; - OIDCRESPONSETYPE_ID_TOKEN_TOKEN = 2; + OIDCRESPONSETYPE_CODE = 0; + OIDCRESPONSETYPE_ID_TOKEN = 1; + OIDCRESPONSETYPE_ID_TOKEN_TOKEN = 2; } message User { - string id = 1; - UserState state = 2; - google.protobuf.Timestamp creation_date = 3; - google.protobuf.Timestamp activation_date = 4; - google.protobuf.Timestamp change_date = 5; - google.protobuf.Timestamp last_login = 6; - google.protobuf.Timestamp password_changed = 7; - string user_name = 8; - string first_name = 9; - string last_name = 10; - string nick_name = 11; - string display_name = 12; - string preferred_language = 13; - Gender gender = 14; - string email = 15; - bool is_email_verified = 16; - string phone = 17; - bool is_phone_verified = 18; - string country = 19; - string locality = 20; - string postal_code = 21; - string region = 22; - string street_address = 23; - bool password_change_required = 24; - uint64 sequence = 25; + string id = 1; + UserState state = 2; + google.protobuf.Timestamp creation_date = 3; + google.protobuf.Timestamp activation_date = 4; + google.protobuf.Timestamp change_date = 5; + google.protobuf.Timestamp last_login = 6; + google.protobuf.Timestamp password_changed = 7; + string user_name = 8; + string first_name = 9; + string last_name = 10; + string nick_name = 11; + string display_name = 12; + string preferred_language = 13; + Gender gender = 14; + string email = 15; + bool is_email_verified = 16; + string phone = 17; + bool is_phone_verified = 18; + string country = 19; + string locality = 20; + string postal_code = 21; + string region = 22; + string street_address = 23; + bool password_change_required = 24; + uint64 sequence = 25; } enum UserState { - USERSTATE_UNSPECIEFIED = 0; - USERSTATE_ACTIVE = 1; - USERSTATE_INACTIVE = 2; - USERSTATE_DELETED = 3; - USERSTATE_LOCKED = 4; - USERSTATE_SUSPEND = 5; - USERSTATE_INITIAL= 6; + USERSTATE_UNSPECIEFIED = 0; + USERSTATE_ACTIVE = 1; + USERSTATE_INACTIVE = 2; + USERSTATE_DELETED = 3; + USERSTATE_LOCKED = 4; + USERSTATE_SUSPEND = 5; + USERSTATE_INITIAL = 6; } enum Gender { - GENDER_UNSPECIFIED = 0; - GENDER_FEMALE = 1; - GENDER_MALE = 2; - GENDER_DIVERSE = 3; + GENDER_UNSPECIFIED = 0; + GENDER_FEMALE = 1; + GENDER_MALE = 2; + GENDER_DIVERSE = 3; } message UserProfile { - string id = 1; - string user_name = 2; - string first_name = 3; - string last_name = 4; - string nick_name = 5; - string display_name = 6; - string preferred_language = 7; - Gender gender = 8; - uint64 sequence = 26; + string id = 1; + string user_name = 2; + string first_name = 3; + string last_name = 4; + string nick_name = 5; + string display_name = 6; + string preferred_language = 7; + Gender gender = 8; + uint64 sequence = 9; + google.protobuf.Timestamp creation_date = 10; + google.protobuf.Timestamp change_date = 11; } message UpdateUserProfileRequest { - string first_name = 1 [(validate.rules).string = {min_len: 1, max_len: 200}]; - string last_name = 2 [(validate.rules).string = {min_len: 1, max_len: 200}]; - string nick_name = 3 [(validate.rules).string = {min_len: 1, max_len: 200}]; - string display_name = 4 [(validate.rules).string = {min_len: 1, max_len: 200}]; - string preferred_language = 5 [(validate.rules).string = {min_len: 1, max_len: 200}]; - Gender gender = 6; + string first_name = 1 [(validate.rules).string = {min_len: 1, max_len: 200}]; + string last_name = 2 [(validate.rules).string = {min_len: 1, max_len: 200}]; + string nick_name = 3 [(validate.rules).string = {min_len: 1, max_len: 200}]; + string display_name = 4 [(validate.rules).string = {min_len: 1, max_len: 200}]; + string preferred_language = 5 [(validate.rules).string = {min_len: 1, max_len: 200}]; + Gender gender = 6; } message UserEmail { - string id = 1; - string email = 2; - bool isEmailVerified = 3; - uint64 sequence = 4; + string id = 1; + string email = 2; + bool isEmailVerified = 3; + uint64 sequence = 4; + google.protobuf.Timestamp creation_date = 5; + google.protobuf.Timestamp change_date = 6; } message VerifyMyUserEmailRequest { - string code = 1 [(validate.rules).string = {min_len: 1, max_len: 200}]; + string code = 1 [(validate.rules).string = {min_len: 1, max_len: 200}]; } message VerifyUserEmailRequest { - string id = 1; - string code = 2 [(validate.rules).string = {min_len: 1, max_len: 200}]; + string id = 1; + string code = 2 [(validate.rules).string = {min_len: 1, max_len: 200}]; } message UpdateUserEmailRequest { - string email = 1 [(validate.rules).string = {min_len: 1, max_len: 200}]; + string email = 1 [(validate.rules).string = {min_len: 1, max_len: 200}]; } message UserPhone { - string id = 1; - string phone = 2; - bool is_phone_verified = 3; - uint64 sequence = 4; + string id = 1; + string phone = 2; + bool is_phone_verified = 3; + uint64 sequence = 4; + google.protobuf.Timestamp creation_date = 5; + google.protobuf.Timestamp change_date = 6; } message UpdateUserPhoneRequest { - string phone = 1 [(validate.rules).string = {min_len: 1, max_len: 20}]; + string phone = 1 [(validate.rules).string = {min_len: 1, max_len: 20}]; } message VerifyUserPhoneRequest { - string code = 1 [(validate.rules).string = {min_len: 1, max_len: 200}]; + string code = 1 [(validate.rules).string = {min_len: 1, max_len: 200}]; } message UserAddress { - string id = 1; - string country = 2; - string locality = 3; - string postal_code = 4; - string region = 5; - string street_address = 6; - uint64 sequence = 7; + string id = 1; + string country = 2; + string locality = 3; + string postal_code = 4; + string region = 5; + string street_address = 6; + uint64 sequence = 7; + google.protobuf.Timestamp creation_date = 8; + google.protobuf.Timestamp change_date = 9; } message UpdateUserAddressRequest { - string country = 1 [(validate.rules).string = {max_len: 200}]; - string locality = 2 [(validate.rules).string = {max_len: 200}]; - string postal_code = 3 [(validate.rules).string = {max_len: 200}]; - string region = 4 [(validate.rules).string = {max_len: 200}]; - string street_address = 5 [(validate.rules).string = {max_len: 200}]; + string country = 1 [(validate.rules).string = {max_len: 200}]; + string locality = 2 [(validate.rules).string = {max_len: 200}]; + string postal_code = 3 [(validate.rules).string = {max_len: 200}]; + string region = 4 [(validate.rules).string = {max_len: 200}]; + string street_address = 5 [(validate.rules).string = {max_len: 200}]; } message PasswordID { - string id = 1; + string id = 1; } message PasswordRequest { - string password = 1 [(validate.rules).string = {min_len: 1, max_len: 72}]; + string password = 1 [(validate.rules).string = {min_len: 1, max_len: 72}]; } message PasswordChange { - string old_password = 1 [(validate.rules).string = {min_len: 1, max_len: 72}]; - string new_password = 2 [(validate.rules).string = {min_len: 1, max_len: 72}]; + string old_password = 1 [(validate.rules).string = {min_len: 1, max_len: 72}]; + string new_password = 2 [(validate.rules).string = {min_len: 1, max_len: 72}]; } enum MfaType { - MFATYPE_UNSPECIFIED = 0; - MFATYPE_SMS = 1; - MFATYPE_OTP = 2; + MFATYPE_UNSPECIFIED = 0; + MFATYPE_SMS = 1; + MFATYPE_OTP = 2; } message VerifyMfaOtp { - string code = 1; + string code = 1; } message MultiFactors { - repeated MultiFactor mfas = 1; + repeated MultiFactor mfas = 1; } message MultiFactor { - MfaType type = 1; - MFAState state = 2; + MfaType type = 1; + MFAState state = 2; } message MfaOtpResponse { - string user_id = 1; - string url = 2; - string secret = 3; - MFAState state = 4; + string user_id = 1; + string url = 2; + string secret = 3; + MFAState state = 4; } enum MFAState { - MFASTATE_UNSPECIFIED = 0; - MFASTATE_NOT_READY = 1; - MFASTATE_READY = 2; - MFASTATE_REMOVED = 3; + MFASTATE_UNSPECIFIED = 0; + MFASTATE_NOT_READY = 1; + MFASTATE_READY = 2; + MFASTATE_REMOVED = 3; } message OIDCClientAuth { - string client_id = 1; - string client_secret = 2; + string client_id = 1; + string client_secret = 2; } message MyProjectOrgSearchRequest { - uint64 offset = 1; - uint64 limit = 2; - bool asc = 4; - repeated MyProjectOrgSearchQuery queries = 5; + uint64 offset = 1; + uint64 limit = 2; + bool asc = 4; + repeated MyProjectOrgSearchQuery queries = 5; } message MyProjectOrgSearchQuery { - MyProjectOrgSearchKey key = 1 [(validate.rules).enum = {not_in: [0]}];; - SearchMethod method = 2; - string value = 3; + MyProjectOrgSearchKey key = 1 [(validate.rules).enum = {not_in: [0]}];; + SearchMethod method = 2; + string value = 3; } enum MyProjectOrgSearchKey { - MYPROJECTORGSEARCHKEY_UNSPECIFIED = 0; - MYPROJECTORGSEARCHKEY_ORG_NAME = 1; + MYPROJECTORGSEARCHKEY_UNSPECIFIED = 0; + MYPROJECTORGSEARCHKEY_ORG_NAME = 1; } message MyProjectOrgSearchResponse { - uint64 offset = 1; - uint64 limit = 2; - uint64 total_result = 3; - repeated Org result = 4; + uint64 offset = 1; + uint64 limit = 2; + uint64 total_result = 3; + repeated Org result = 4; } message IsAdminResponse { - bool is_admin = 1; + bool is_admin = 1; } message Org { - string id = 1; - string name = 2; + string id = 1; + string name = 2; } message MyPermissions { - repeated string permissions = 1; + repeated string permissions = 1; } enum SearchMethod { - SEARCHMETHOD_EQUALS = 0; - SEARCHMETHOD_STARTS_WITH = 1; - SEARCHMETHOD_CONTAINS = 2; + SEARCHMETHOD_EQUALS = 0; + SEARCHMETHOD_STARTS_WITH = 1; + SEARCHMETHOD_CONTAINS = 2; } \ No newline at end of file diff --git a/pkg/auth/auth.go b/pkg/auth/auth.go index 42d98871e0..513ef76e03 100644 --- a/pkg/auth/auth.go +++ b/pkg/auth/auth.go @@ -2,17 +2,23 @@ package auth import ( "context" + + "github.com/caos/logging" + "github.com/caos/zitadel/internal/api/auth" - app "github.com/caos/zitadel/internal/auth" + "github.com/caos/zitadel/internal/auth/repository/eventsourcing" sd "github.com/caos/zitadel/internal/config/systemdefaults" "github.com/caos/zitadel/pkg/auth/api" ) type Config struct { - App app.Config - API api.Config + API api.Config + Repository eventsourcing.Config } func Start(ctx context.Context, config Config, authZ auth.Config, systemDefaults sd.SystemDefaults) { - api.Start(ctx, config.API) + repo, err := eventsourcing.Start(config.Repository, systemDefaults) + logging.Log("MAIN-9uBxp").OnError(err).Panic("unable to start app") + + api.Start(ctx, config.API, authZ, repo) }