mirror of
https://github.com/zitadel/zitadel.git
synced 2025-08-12 11:27:33 +00:00
feat: add auth command side (#107)
* fix: query tests * fix: use prepare funcs * fix: go mod * fix: generate files * fix(eventstore): tests * fix(eventstore): rename modifier to editor * fix(migrations): add cluster migration, fix(migrations): fix typo of host in clean clsuter * fix(eventstore): move health * fix(eventstore): AggregateTypeFilter aggregateType as param * code quality * fix: go tests * feat: add member funcs * feat: add member model * feat: add member events * feat: add member repo model * fix: better error func testing * fix: project member funcs * fix: add tests * fix: add tests * feat: implement member requests * fix: merge master * fix: merge master * fix: read existing in project repo * fix: fix tests * feat: add internal cache * feat: add cache mock * fix: return values of cache mock * feat: add project role * fix: add cache config * fix: add role to eventstore * fix: use eventstore sdk * fix: use eventstore sdk * fix: add project role grpc requests * fix: fix getby id * fix: changes for mr * fix: change value to interface * feat: add app event creations * fix: searchmethods * Update internal/project/model/project_member.go Co-Authored-By: Silvan <silvan.reusser@gmail.com> * fix: use get project func * fix: append events * fix: check if value is string on equal ignore case * fix: add changes test * fix: add go mod * fix: add some tests * fix: return err not nil * fix: return err not nil * fix: add aggregate funcs and tests * fix: add oidc aggregate funcs and tests * fix: add oidc * fix: add some tests * fix: tests * feat: eventstore repository * fix: remove gorm * version * feat: pkg * feat: eventstore without eventstore-lib * rename files * gnueg * fix: global model * feat: add global view functions * feat(eventstore): sdk * fix(eventstore): rename app to eventstore * delete empty test * fix(models): delete unused struct * feat(eventstore): overwrite context data * fix: use global sql config * fix: oidc validation * fix: generate client secret * fix: generate client id * fix: test change app * fix: deactivate/reactivate application * fix: change oidc config * fix: change oidc config secret * begin models * begin repo * fix: implement grpc app funcs * fix: add application requests * fix: converter * fix: converter * fix: converter and generate clientid * fix: tests * feat: project grant aggregate * feat: project grant * fix: project grant check if role existing * fix: project grant requests * fix: project grant fixes * fix: project grant member model * fix: project grant member aggregate * fix: project grant member eventstore * fix: project grant member requests * feat: user model * begin repo * repo models and more * feat: user command side * lots of functions * user command side * profile requests * commit before rebase on user * save * local config with gopass and more * begin new auth command (user centric) * Update internal/user/model/user.go Co-Authored-By: Livio Amstutz <livio.a@gmail.com> * Update internal/user/repository/eventsourcing/model/address.go Co-Authored-By: Livio Amstutz <livio.a@gmail.com> * Update internal/user/repository/eventsourcing/model/address.go Co-Authored-By: Livio Amstutz <livio.a@gmail.com> * Update internal/user/repository/eventsourcing/model/email.go Co-Authored-By: Livio Amstutz <livio.a@gmail.com> * Update internal/user/repository/eventsourcing/model/email.go Co-Authored-By: Livio Amstutz <livio.a@gmail.com> * Update internal/user/repository/eventsourcing/model/email.go Co-Authored-By: Livio Amstutz <livio.a@gmail.com> * Update internal/user/repository/eventsourcing/model/mfa.go Co-Authored-By: Livio Amstutz <livio.a@gmail.com> * Update internal/user/repository/eventsourcing/model/mfa.go Co-Authored-By: Livio Amstutz <livio.a@gmail.com> * Update internal/user/repository/eventsourcing/model/password.go Co-Authored-By: Livio Amstutz <livio.a@gmail.com> * Update internal/user/repository/eventsourcing/model/password.go Co-Authored-By: Livio Amstutz <livio.a@gmail.com> * Update internal/user/repository/eventsourcing/model/password.go Co-Authored-By: Livio Amstutz <livio.a@gmail.com> * Update internal/user/repository/eventsourcing/model/phone.go Co-Authored-By: Livio Amstutz <livio.a@gmail.com> * Update internal/user/repository/eventsourcing/model/phone.go Co-Authored-By: Livio Amstutz <livio.a@gmail.com> * Update internal/user/repository/eventsourcing/model/phone.go Co-Authored-By: Livio Amstutz <livio.a@gmail.com> * Update internal/user/repository/eventsourcing/model/user.go Co-Authored-By: Livio Amstutz <livio.a@gmail.com> * Update internal/user/repository/eventsourcing/model/user.go Co-Authored-By: Livio Amstutz <livio.a@gmail.com> * Update internal/user/repository/eventsourcing/model/user.go Co-Authored-By: Livio Amstutz <livio.a@gmail.com> * Update internal/usergrant/repository/eventsourcing/model/user_grant.go Co-Authored-By: Livio Amstutz <livio.a@gmail.com> * Update internal/usergrant/repository/eventsourcing/model/user_grant.go Co-Authored-By: Livio Amstutz <livio.a@gmail.com> * Update internal/usergrant/repository/eventsourcing/user_grant.go Co-Authored-By: Livio Amstutz <livio.a@gmail.com> * Update internal/user/repository/eventsourcing/user_test.go Co-Authored-By: Livio Amstutz <livio.a@gmail.com> * Update internal/user/repository/eventsourcing/eventstore_mock_test.go Co-Authored-By: Livio Amstutz <livio.a@gmail.com> * changes from mr review * save files into basedir * changes from mr review * changes from mr review * move to auth request * Update internal/usergrant/repository/eventsourcing/cache.go Co-authored-by: Silvan <silvan.reusser@gmail.com> * Update internal/usergrant/repository/eventsourcing/cache.go Co-authored-by: Silvan <silvan.reusser@gmail.com> * changes requested on mr * fix generate codes * fix return if no events * password code * email verification step * more steps * lot of mfa * begin tests * more next steps * auth api * auth api (user) * auth api (user) * auth api (user) * differ requests * merge * tests * fix compilation error * mock for id generator * Update internal/user/repository/eventsourcing/model/password.go Co-authored-by: Silvan <silvan.reusser@gmail.com> * Update internal/user/repository/eventsourcing/model/user.go Co-authored-by: Silvan <silvan.reusser@gmail.com> * requests of mr * check email * begin separation of command and query * otp * change packages * some cleanup and fixes * tests for auth request / next steps * add VerificationLifetimes to config and make it run * tests * fix code challenge validation * cleanup * fix merge * begin view * repackaging tests and configs * fix startup config for auth * add migration * add PromptSelectAccount * fix copy / paste * remove user_agent files * fixes * fix sequences in user_session * token commands * token queries and signout * fix * fix set password test * add token handler and table * handle session init * add session state * add user view test cases * change VerifyMyMfaOTP * some fixes * fix user repo in auth api * cleanup * add user session view test * fix merge * fixes * Update internal/auth/repository/eventsourcing/eventstore/auth_request.go Co-authored-by: Fabi <38692350+fgerschwiler@users.noreply.github.com> * Update internal/auth/repository/eventsourcing/eventstore/auth_request.go Co-authored-by: Fabi <38692350+fgerschwiler@users.noreply.github.com> * Update internal/auth/repository/eventsourcing/eventstore/auth_request.go Co-authored-by: Fabi <38692350+fgerschwiler@users.noreply.github.com> * Update internal/auth/repository/eventsourcing/eventstore/auth_request.go Co-authored-by: Fabi <38692350+fgerschwiler@users.noreply.github.com> * extract method usersForUserSelection * add todo for policy check * id on auth req * fix enum name Co-authored-by: Fabiennne <fabienne.gerschwiler@gmail.com> Co-authored-by: adlerhurst <silvan.reusser@gmail.com> Co-authored-by: Fabi <38692350+fgerschwiler@users.noreply.github.com>
This commit is contained in:
15
internal/auth/repository/auth_request.go
Normal file
15
internal/auth/repository/auth_request.go
Normal file
@@ -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
|
||||
}
|
@@ -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
|
||||
}
|
@@ -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)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
26
internal/auth/repository/eventsourcing/eventstore/token.go
Normal file
26
internal/auth/repository/eventsourcing/eventstore/token.go
Normal file
@@ -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)
|
||||
}
|
129
internal/auth/repository/eventsourcing/eventstore/user.go
Normal file
129
internal/auth/repository/eventsourcing/eventstore/user.go
Normal file
@@ -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
|
||||
}
|
42
internal/auth/repository/eventsourcing/handler/handler.go
Normal file
42
internal/auth/repository/eventsourcing/handler/handler.go
Normal file
@@ -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
|
||||
}
|
69
internal/auth/repository/eventsourcing/handler/token.go
Normal file
69
internal/auth/repository/eventsourcing/handler/token.go
Normal file
@@ -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
|
||||
}
|
77
internal/auth/repository/eventsourcing/handler/user.go
Normal file
77
internal/auth/repository/eventsourcing/handler/user.go
Normal file
@@ -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)
|
||||
}
|
@@ -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
|
||||
}
|
93
internal/auth/repository/eventsourcing/repository.go
Normal file
93
internal/auth/repository/eventsourcing/repository.go
Normal file
@@ -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)
|
||||
}
|
46
internal/auth/repository/eventsourcing/spooler/lock.go
Normal file
46
internal/auth/repository/eventsourcing/spooler/lock.go
Normal file
@@ -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
|
||||
})
|
||||
}
|
127
internal/auth/repository/eventsourcing/spooler/lock_test.go
Normal file
127
internal/auth/repository/eventsourcing/spooler/lock_test.go
Normal file
@@ -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)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
30
internal/auth/repository/eventsourcing/spooler/spooler.go
Normal file
30
internal/auth/repository/eventsourcing/spooler/spooler.go
Normal file
@@ -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
|
||||
}
|
17
internal/auth/repository/eventsourcing/view/error_event.go
Normal file
17
internal/auth/repository/eventsourcing/view/error_event.go
Normal file
@@ -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)
|
||||
}
|
17
internal/auth/repository/eventsourcing/view/sequence.go
Normal file
17
internal/auth/repository/eventsourcing/view/sequence.go
Normal file
@@ -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)
|
||||
}
|
77
internal/auth/repository/eventsourcing/view/token.go
Normal file
77
internal/auth/repository/eventsourcing/view/token.go
Normal file
@@ -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)
|
||||
}
|
68
internal/auth/repository/eventsourcing/view/user.go
Normal file
68
internal/auth/repository/eventsourcing/view/user.go
Normal file
@@ -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)
|
||||
}
|
55
internal/auth/repository/eventsourcing/view/user_session.go
Normal file
55
internal/auth/repository/eventsourcing/view/user_session.go
Normal file
@@ -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)
|
||||
}
|
25
internal/auth/repository/eventsourcing/view/view.go
Normal file
25
internal/auth/repository/eventsourcing/view/view.go
Normal file
@@ -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()
|
||||
}
|
12
internal/auth/repository/repository.go
Normal file
12
internal/auth/repository/repository.go
Normal file
@@ -0,0 +1,12 @@
|
||||
package repository
|
||||
|
||||
import (
|
||||
"context"
|
||||
)
|
||||
|
||||
type Repository interface {
|
||||
Health(context.Context) error
|
||||
UserRepository
|
||||
AuthRequestRepository
|
||||
TokenRepository
|
||||
}
|
13
internal/auth/repository/token.go
Normal file
13
internal/auth/repository/token.go
Normal file
@@ -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)
|
||||
}
|
42
internal/auth/repository/user.go
Normal file
42
internal/auth/repository/user.go
Normal file
@@ -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
|
||||
}
|
Reference in New Issue
Block a user