feat(relational): implement session repository (#11109)

# Which Problems Are Solved

As part of going to a relation storage model, we need to implement the
repository for handling sessions and their events.

# How the Problems Are Solved

- Adds a migration step to create the necessary tables and types in the
database.
- Adds the repository implementation for sessions.
- Implements a projection handler for session events to handle the
events written by the old command handlers.
- Updates the `Session` type and `SessionRepository` interface:
  - added `CreatorID`
  - Adjusted `Delete` method to return amount of affected columns
  - Removed sub-repository clumns from the main interface
- Added `CreatorIDCondition`, `ExpirationCondition`, `ExistsFactor` and
`ExistsMetadata`
  - Removed `SetUserAgent`
- Methods of `SessionFactor` and `SessionChallenge` have been made
public
- fixed enumer on `SessionFactorType`
- Adds a `JSON` type to the repository package to easy can JSON columns,
resp. jsonb object inside a property of a `JSONArray` object.
- Adds a `NoChange` interface, which can be implemented to signal that a
`Change` interface implementation is not writing any changes to the
statement builder, e.g. `CTEChange`

# Additional Changes

- Fixes the operators enums. `TextOperations` had wrong comments and
`NumberOperations` additionally also had from comparisons.
- Fixes downgrade of migration step 008.

# Additional Context

- closes https://github.com/zitadel/zitadel/issues/10212
This commit is contained in:
Livio Spring
2026-01-06 08:08:11 +01:00
committed by GitHub
parent 5f34d1af8f
commit 4e6f68c511
25 changed files with 4305 additions and 387 deletions
+216 -254
View File
@@ -23,7 +23,6 @@ import (
type MockSessionRepository struct {
ctrl *gomock.Controller
recorder *MockSessionRepositoryMockRecorder
isgomock struct{}
}
// MockSessionRepositoryMockRecorder is the mock recorder for MockSessionRepository.
@@ -44,17 +43,17 @@ func (m *MockSessionRepository) EXPECT() *MockSessionRepositoryMockRecorder {
}
// ClearFactor mocks base method.
func (m *MockSessionRepository) ClearFactor(factor domain.SessionFactorType) database.Change {
func (m *MockSessionRepository) ClearFactor(arg0 domain.SessionFactorType) database.Change {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "ClearFactor", factor)
ret := m.ctrl.Call(m, "ClearFactor", arg0)
ret0, _ := ret[0].(database.Change)
return ret0
}
// ClearFactor indicates an expected call of ClearFactor.
func (mr *MockSessionRepositoryMockRecorder) ClearFactor(factor any) *MockSessionRepositoryClearFactorCall {
func (mr *MockSessionRepositoryMockRecorder) ClearFactor(arg0 any) *MockSessionRepositoryClearFactorCall {
mr.mock.ctrl.T.Helper()
call := mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ClearFactor", reflect.TypeOf((*MockSessionRepository)(nil).ClearFactor), factor)
call := mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ClearFactor", reflect.TypeOf((*MockSessionRepository)(nil).ClearFactor), arg0)
return &MockSessionRepositoryClearFactorCall{Call: call}
}
@@ -82,17 +81,17 @@ func (c *MockSessionRepositoryClearFactorCall) DoAndReturn(f func(domain.Session
}
// Create mocks base method.
func (m *MockSessionRepository) Create(ctx context.Context, client database.QueryExecutor, user *domain.Session) error {
func (m *MockSessionRepository) Create(arg0 context.Context, arg1 database.QueryExecutor, arg2 *domain.Session) error {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "Create", ctx, client, user)
ret := m.ctrl.Call(m, "Create", arg0, arg1, arg2)
ret0, _ := ret[0].(error)
return ret0
}
// Create indicates an expected call of Create.
func (mr *MockSessionRepositoryMockRecorder) Create(ctx, client, user any) *MockSessionRepositoryCreateCall {
func (mr *MockSessionRepositoryMockRecorder) Create(arg0, arg1, arg2 any) *MockSessionRepositoryCreateCall {
mr.mock.ctrl.T.Helper()
call := mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Create", reflect.TypeOf((*MockSessionRepository)(nil).Create), ctx, client, user)
call := mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Create", reflect.TypeOf((*MockSessionRepository)(nil).Create), arg0, arg1, arg2)
return &MockSessionRepositoryCreateCall{Call: call}
}
@@ -158,17 +157,17 @@ func (c *MockSessionRepositoryCreatedAtColumnCall) DoAndReturn(f func() database
}
// CreatedAtCondition mocks base method.
func (m *MockSessionRepository) CreatedAtCondition(op database.NumberOperation, createdAt time.Time) database.Condition {
func (m *MockSessionRepository) CreatedAtCondition(arg0 database.NumberOperation, arg1 time.Time) database.Condition {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "CreatedAtCondition", op, createdAt)
ret := m.ctrl.Call(m, "CreatedAtCondition", arg0, arg1)
ret0, _ := ret[0].(database.Condition)
return ret0
}
// CreatedAtCondition indicates an expected call of CreatedAtCondition.
func (mr *MockSessionRepositoryMockRecorder) CreatedAtCondition(op, createdAt any) *MockSessionRepositoryCreatedAtConditionCall {
func (mr *MockSessionRepositoryMockRecorder) CreatedAtCondition(arg0, arg1 any) *MockSessionRepositoryCreatedAtConditionCall {
mr.mock.ctrl.T.Helper()
call := mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "CreatedAtCondition", reflect.TypeOf((*MockSessionRepository)(nil).CreatedAtCondition), op, createdAt)
call := mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "CreatedAtCondition", reflect.TypeOf((*MockSessionRepository)(nil).CreatedAtCondition), arg0, arg1)
return &MockSessionRepositoryCreatedAtConditionCall{Call: call}
}
@@ -195,18 +194,57 @@ func (c *MockSessionRepositoryCreatedAtConditionCall) DoAndReturn(f func(databas
return c
}
// Delete mocks base method.
func (m *MockSessionRepository) Delete(ctx context.Context, client database.QueryExecutor, condition database.Condition) error {
// CreatorIDCondition mocks base method.
func (m *MockSessionRepository) CreatorIDCondition(arg0 string) database.Condition {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "Delete", ctx, client, condition)
ret0, _ := ret[0].(error)
ret := m.ctrl.Call(m, "CreatorIDCondition", arg0)
ret0, _ := ret[0].(database.Condition)
return ret0
}
// Delete indicates an expected call of Delete.
func (mr *MockSessionRepositoryMockRecorder) Delete(ctx, client, condition any) *MockSessionRepositoryDeleteCall {
// CreatorIDCondition indicates an expected call of CreatorIDCondition.
func (mr *MockSessionRepositoryMockRecorder) CreatorIDCondition(arg0 any) *MockSessionRepositoryCreatorIDConditionCall {
mr.mock.ctrl.T.Helper()
call := mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Delete", reflect.TypeOf((*MockSessionRepository)(nil).Delete), ctx, client, condition)
call := mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "CreatorIDCondition", reflect.TypeOf((*MockSessionRepository)(nil).CreatorIDCondition), arg0)
return &MockSessionRepositoryCreatorIDConditionCall{Call: call}
}
// MockSessionRepositoryCreatorIDConditionCall wrap *gomock.Call
type MockSessionRepositoryCreatorIDConditionCall struct {
*gomock.Call
}
// Return rewrite *gomock.Call.Return
func (c *MockSessionRepositoryCreatorIDConditionCall) Return(arg0 database.Condition) *MockSessionRepositoryCreatorIDConditionCall {
c.Call = c.Call.Return(arg0)
return c
}
// Do rewrite *gomock.Call.Do
func (c *MockSessionRepositoryCreatorIDConditionCall) Do(f func(string) database.Condition) *MockSessionRepositoryCreatorIDConditionCall {
c.Call = c.Call.Do(f)
return c
}
// DoAndReturn rewrite *gomock.Call.DoAndReturn
func (c *MockSessionRepositoryCreatorIDConditionCall) DoAndReturn(f func(string) database.Condition) *MockSessionRepositoryCreatorIDConditionCall {
c.Call = c.Call.DoAndReturn(f)
return c
}
// Delete mocks base method.
func (m *MockSessionRepository) Delete(arg0 context.Context, arg1 database.QueryExecutor, arg2 database.Condition) (int64, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "Delete", arg0, arg1, arg2)
ret0, _ := ret[0].(int64)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// Delete indicates an expected call of Delete.
func (mr *MockSessionRepositoryMockRecorder) Delete(arg0, arg1, arg2 any) *MockSessionRepositoryDeleteCall {
mr.mock.ctrl.T.Helper()
call := mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Delete", reflect.TypeOf((*MockSessionRepository)(nil).Delete), arg0, arg1, arg2)
return &MockSessionRepositoryDeleteCall{Call: call}
}
@@ -216,19 +254,95 @@ type MockSessionRepositoryDeleteCall struct {
}
// Return rewrite *gomock.Call.Return
func (c *MockSessionRepositoryDeleteCall) Return(arg0 error) *MockSessionRepositoryDeleteCall {
c.Call = c.Call.Return(arg0)
func (c *MockSessionRepositoryDeleteCall) Return(arg0 int64, arg1 error) *MockSessionRepositoryDeleteCall {
c.Call = c.Call.Return(arg0, arg1)
return c
}
// Do rewrite *gomock.Call.Do
func (c *MockSessionRepositoryDeleteCall) Do(f func(context.Context, database.QueryExecutor, database.Condition) error) *MockSessionRepositoryDeleteCall {
func (c *MockSessionRepositoryDeleteCall) Do(f func(context.Context, database.QueryExecutor, database.Condition) (int64, error)) *MockSessionRepositoryDeleteCall {
c.Call = c.Call.Do(f)
return c
}
// DoAndReturn rewrite *gomock.Call.DoAndReturn
func (c *MockSessionRepositoryDeleteCall) DoAndReturn(f func(context.Context, database.QueryExecutor, database.Condition) error) *MockSessionRepositoryDeleteCall {
func (c *MockSessionRepositoryDeleteCall) DoAndReturn(f func(context.Context, database.QueryExecutor, database.Condition) (int64, error)) *MockSessionRepositoryDeleteCall {
c.Call = c.Call.DoAndReturn(f)
return c
}
// ExistsFactor mocks base method.
func (m *MockSessionRepository) ExistsFactor(arg0 database.Condition) database.Condition {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "ExistsFactor", arg0)
ret0, _ := ret[0].(database.Condition)
return ret0
}
// ExistsFactor indicates an expected call of ExistsFactor.
func (mr *MockSessionRepositoryMockRecorder) ExistsFactor(arg0 any) *MockSessionRepositoryExistsFactorCall {
mr.mock.ctrl.T.Helper()
call := mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ExistsFactor", reflect.TypeOf((*MockSessionRepository)(nil).ExistsFactor), arg0)
return &MockSessionRepositoryExistsFactorCall{Call: call}
}
// MockSessionRepositoryExistsFactorCall wrap *gomock.Call
type MockSessionRepositoryExistsFactorCall struct {
*gomock.Call
}
// Return rewrite *gomock.Call.Return
func (c *MockSessionRepositoryExistsFactorCall) Return(arg0 database.Condition) *MockSessionRepositoryExistsFactorCall {
c.Call = c.Call.Return(arg0)
return c
}
// Do rewrite *gomock.Call.Do
func (c *MockSessionRepositoryExistsFactorCall) Do(f func(database.Condition) database.Condition) *MockSessionRepositoryExistsFactorCall {
c.Call = c.Call.Do(f)
return c
}
// DoAndReturn rewrite *gomock.Call.DoAndReturn
func (c *MockSessionRepositoryExistsFactorCall) DoAndReturn(f func(database.Condition) database.Condition) *MockSessionRepositoryExistsFactorCall {
c.Call = c.Call.DoAndReturn(f)
return c
}
// ExistsMetadata mocks base method.
func (m *MockSessionRepository) ExistsMetadata(arg0 database.Condition) database.Condition {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "ExistsMetadata", arg0)
ret0, _ := ret[0].(database.Condition)
return ret0
}
// ExistsMetadata indicates an expected call of ExistsMetadata.
func (mr *MockSessionRepositoryMockRecorder) ExistsMetadata(arg0 any) *MockSessionRepositoryExistsMetadataCall {
mr.mock.ctrl.T.Helper()
call := mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ExistsMetadata", reflect.TypeOf((*MockSessionRepository)(nil).ExistsMetadata), arg0)
return &MockSessionRepositoryExistsMetadataCall{Call: call}
}
// MockSessionRepositoryExistsMetadataCall wrap *gomock.Call
type MockSessionRepositoryExistsMetadataCall struct {
*gomock.Call
}
// Return rewrite *gomock.Call.Return
func (c *MockSessionRepositoryExistsMetadataCall) Return(arg0 database.Condition) *MockSessionRepositoryExistsMetadataCall {
c.Call = c.Call.Return(arg0)
return c
}
// Do rewrite *gomock.Call.Do
func (c *MockSessionRepositoryExistsMetadataCall) Do(f func(database.Condition) database.Condition) *MockSessionRepositoryExistsMetadataCall {
c.Call = c.Call.Do(f)
return c
}
// DoAndReturn rewrite *gomock.Call.DoAndReturn
func (c *MockSessionRepositoryExistsMetadataCall) DoAndReturn(f func(database.Condition) database.Condition) *MockSessionRepositoryExistsMetadataCall {
c.Call = c.Call.DoAndReturn(f)
return c
}
@@ -271,40 +385,40 @@ func (c *MockSessionRepositoryExpirationColumnCall) DoAndReturn(f func() databas
return c
}
// FactorColumns mocks base method.
func (m *MockSessionRepository) FactorColumns() domain.SessionFactorColumns {
// ExpirationCondition mocks base method.
func (m *MockSessionRepository) ExpirationCondition(arg0 database.NumberOperation, arg1 time.Time) database.Condition {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "FactorColumns")
ret0, _ := ret[0].(domain.SessionFactorColumns)
ret := m.ctrl.Call(m, "ExpirationCondition", arg0, arg1)
ret0, _ := ret[0].(database.Condition)
return ret0
}
// FactorColumns indicates an expected call of FactorColumns.
func (mr *MockSessionRepositoryMockRecorder) FactorColumns() *MockSessionRepositoryFactorColumnsCall {
// ExpirationCondition indicates an expected call of ExpirationCondition.
func (mr *MockSessionRepositoryMockRecorder) ExpirationCondition(arg0, arg1 any) *MockSessionRepositoryExpirationConditionCall {
mr.mock.ctrl.T.Helper()
call := mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "FactorColumns", reflect.TypeOf((*MockSessionRepository)(nil).FactorColumns))
return &MockSessionRepositoryFactorColumnsCall{Call: call}
call := mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ExpirationCondition", reflect.TypeOf((*MockSessionRepository)(nil).ExpirationCondition), arg0, arg1)
return &MockSessionRepositoryExpirationConditionCall{Call: call}
}
// MockSessionRepositoryFactorColumnsCall wrap *gomock.Call
type MockSessionRepositoryFactorColumnsCall struct {
// MockSessionRepositoryExpirationConditionCall wrap *gomock.Call
type MockSessionRepositoryExpirationConditionCall struct {
*gomock.Call
}
// Return rewrite *gomock.Call.Return
func (c *MockSessionRepositoryFactorColumnsCall) Return(arg0 domain.SessionFactorColumns) *MockSessionRepositoryFactorColumnsCall {
func (c *MockSessionRepositoryExpirationConditionCall) Return(arg0 database.Condition) *MockSessionRepositoryExpirationConditionCall {
c.Call = c.Call.Return(arg0)
return c
}
// Do rewrite *gomock.Call.Do
func (c *MockSessionRepositoryFactorColumnsCall) Do(f func() domain.SessionFactorColumns) *MockSessionRepositoryFactorColumnsCall {
func (c *MockSessionRepositoryExpirationConditionCall) Do(f func(database.NumberOperation, time.Time) database.Condition) *MockSessionRepositoryExpirationConditionCall {
c.Call = c.Call.Do(f)
return c
}
// DoAndReturn rewrite *gomock.Call.DoAndReturn
func (c *MockSessionRepositoryFactorColumnsCall) DoAndReturn(f func() domain.SessionFactorColumns) *MockSessionRepositoryFactorColumnsCall {
func (c *MockSessionRepositoryExpirationConditionCall) DoAndReturn(f func(database.NumberOperation, time.Time) database.Condition) *MockSessionRepositoryExpirationConditionCall {
c.Call = c.Call.DoAndReturn(f)
return c
}
@@ -348,10 +462,10 @@ func (c *MockSessionRepositoryFactorConditionsCall) DoAndReturn(f func() domain.
}
// Get mocks base method.
func (m *MockSessionRepository) Get(ctx context.Context, client database.QueryExecutor, opts ...database.QueryOption) (*domain.Session, error) {
func (m *MockSessionRepository) Get(arg0 context.Context, arg1 database.QueryExecutor, arg2 ...database.QueryOption) (*domain.Session, error) {
m.ctrl.T.Helper()
varargs := []any{ctx, client}
for _, a := range opts {
varargs := []any{arg0, arg1}
for _, a := range arg2 {
varargs = append(varargs, a)
}
ret := m.ctrl.Call(m, "Get", varargs...)
@@ -361,9 +475,9 @@ func (m *MockSessionRepository) Get(ctx context.Context, client database.QueryEx
}
// Get indicates an expected call of Get.
func (mr *MockSessionRepositoryMockRecorder) Get(ctx, client any, opts ...any) *MockSessionRepositoryGetCall {
func (mr *MockSessionRepositoryMockRecorder) Get(arg0, arg1 any, arg2 ...any) *MockSessionRepositoryGetCall {
mr.mock.ctrl.T.Helper()
varargs := append([]any{ctx, client}, opts...)
varargs := append([]any{arg0, arg1}, arg2...)
call := mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Get", reflect.TypeOf((*MockSessionRepository)(nil).Get), varargs...)
return &MockSessionRepositoryGetCall{Call: call}
}
@@ -430,17 +544,17 @@ func (c *MockSessionRepositoryIDColumnCall) DoAndReturn(f func() database.Column
}
// IDCondition mocks base method.
func (m *MockSessionRepository) IDCondition(sessionID string) database.Condition {
func (m *MockSessionRepository) IDCondition(arg0 string) database.Condition {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "IDCondition", sessionID)
ret := m.ctrl.Call(m, "IDCondition", arg0)
ret0, _ := ret[0].(database.Condition)
return ret0
}
// IDCondition indicates an expected call of IDCondition.
func (mr *MockSessionRepositoryMockRecorder) IDCondition(sessionID any) *MockSessionRepositoryIDConditionCall {
func (mr *MockSessionRepositoryMockRecorder) IDCondition(arg0 any) *MockSessionRepositoryIDConditionCall {
mr.mock.ctrl.T.Helper()
call := mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "IDCondition", reflect.TypeOf((*MockSessionRepository)(nil).IDCondition), sessionID)
call := mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "IDCondition", reflect.TypeOf((*MockSessionRepository)(nil).IDCondition), arg0)
return &MockSessionRepositoryIDConditionCall{Call: call}
}
@@ -506,17 +620,17 @@ func (c *MockSessionRepositoryInstanceIDColumnCall) DoAndReturn(f func() databas
}
// InstanceIDCondition mocks base method.
func (m *MockSessionRepository) InstanceIDCondition(instanceID string) database.Condition {
func (m *MockSessionRepository) InstanceIDCondition(arg0 string) database.Condition {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "InstanceIDCondition", instanceID)
ret := m.ctrl.Call(m, "InstanceIDCondition", arg0)
ret0, _ := ret[0].(database.Condition)
return ret0
}
// InstanceIDCondition indicates an expected call of InstanceIDCondition.
func (mr *MockSessionRepositoryMockRecorder) InstanceIDCondition(instanceID any) *MockSessionRepositoryInstanceIDConditionCall {
func (mr *MockSessionRepositoryMockRecorder) InstanceIDCondition(arg0 any) *MockSessionRepositoryInstanceIDConditionCall {
mr.mock.ctrl.T.Helper()
call := mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "InstanceIDCondition", reflect.TypeOf((*MockSessionRepository)(nil).InstanceIDCondition), instanceID)
call := mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "InstanceIDCondition", reflect.TypeOf((*MockSessionRepository)(nil).InstanceIDCondition), arg0)
return &MockSessionRepositoryInstanceIDConditionCall{Call: call}
}
@@ -582,10 +696,10 @@ func (c *MockSessionRepositoryLifetimeColumnCall) DoAndReturn(f func() database.
}
// List mocks base method.
func (m *MockSessionRepository) List(ctx context.Context, client database.QueryExecutor, opts ...database.QueryOption) ([]*domain.Session, error) {
func (m *MockSessionRepository) List(arg0 context.Context, arg1 database.QueryExecutor, arg2 ...database.QueryOption) ([]*domain.Session, error) {
m.ctrl.T.Helper()
varargs := []any{ctx, client}
for _, a := range opts {
varargs := []any{arg0, arg1}
for _, a := range arg2 {
varargs = append(varargs, a)
}
ret := m.ctrl.Call(m, "List", varargs...)
@@ -595,9 +709,9 @@ func (m *MockSessionRepository) List(ctx context.Context, client database.QueryE
}
// List indicates an expected call of List.
func (mr *MockSessionRepositoryMockRecorder) List(ctx, client any, opts ...any) *MockSessionRepositoryListCall {
func (mr *MockSessionRepositoryMockRecorder) List(arg0, arg1 any, arg2 ...any) *MockSessionRepositoryListCall {
mr.mock.ctrl.T.Helper()
varargs := append([]any{ctx, client}, opts...)
varargs := append([]any{arg0, arg1}, arg2...)
call := mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "List", reflect.TypeOf((*MockSessionRepository)(nil).List), varargs...)
return &MockSessionRepositoryListCall{Call: call}
}
@@ -625,44 +739,6 @@ func (c *MockSessionRepositoryListCall) DoAndReturn(f func(context.Context, data
return c
}
// MetadataColumns mocks base method.
func (m *MockSessionRepository) MetadataColumns() domain.SessionMetadataColumns {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "MetadataColumns")
ret0, _ := ret[0].(domain.SessionMetadataColumns)
return ret0
}
// MetadataColumns indicates an expected call of MetadataColumns.
func (mr *MockSessionRepositoryMockRecorder) MetadataColumns() *MockSessionRepositoryMetadataColumnsCall {
mr.mock.ctrl.T.Helper()
call := mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "MetadataColumns", reflect.TypeOf((*MockSessionRepository)(nil).MetadataColumns))
return &MockSessionRepositoryMetadataColumnsCall{Call: call}
}
// MockSessionRepositoryMetadataColumnsCall wrap *gomock.Call
type MockSessionRepositoryMetadataColumnsCall struct {
*gomock.Call
}
// Return rewrite *gomock.Call.Return
func (c *MockSessionRepositoryMetadataColumnsCall) Return(arg0 domain.SessionMetadataColumns) *MockSessionRepositoryMetadataColumnsCall {
c.Call = c.Call.Return(arg0)
return c
}
// Do rewrite *gomock.Call.Do
func (c *MockSessionRepositoryMetadataColumnsCall) Do(f func() domain.SessionMetadataColumns) *MockSessionRepositoryMetadataColumnsCall {
c.Call = c.Call.Do(f)
return c
}
// DoAndReturn rewrite *gomock.Call.DoAndReturn
func (c *MockSessionRepositoryMetadataColumnsCall) DoAndReturn(f func() domain.SessionMetadataColumns) *MockSessionRepositoryMetadataColumnsCall {
c.Call = c.Call.DoAndReturn(f)
return c
}
// MetadataConditions mocks base method.
func (m *MockSessionRepository) MetadataConditions() domain.SessionMetadataConditions {
m.ctrl.T.Helper()
@@ -740,17 +816,17 @@ func (c *MockSessionRepositoryPrimaryKeyColumnsCall) DoAndReturn(f func() []data
}
// PrimaryKeyCondition mocks base method.
func (m *MockSessionRepository) PrimaryKeyCondition(instanceID, sessionID string) database.Condition {
func (m *MockSessionRepository) PrimaryKeyCondition(arg0, arg1 string) database.Condition {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "PrimaryKeyCondition", instanceID, sessionID)
ret := m.ctrl.Call(m, "PrimaryKeyCondition", arg0, arg1)
ret0, _ := ret[0].(database.Condition)
return ret0
}
// PrimaryKeyCondition indicates an expected call of PrimaryKeyCondition.
func (mr *MockSessionRepositoryMockRecorder) PrimaryKeyCondition(instanceID, sessionID any) *MockSessionRepositoryPrimaryKeyConditionCall {
func (mr *MockSessionRepositoryMockRecorder) PrimaryKeyCondition(arg0, arg1 any) *MockSessionRepositoryPrimaryKeyConditionCall {
mr.mock.ctrl.T.Helper()
call := mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "PrimaryKeyCondition", reflect.TypeOf((*MockSessionRepository)(nil).PrimaryKeyCondition), instanceID, sessionID)
call := mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "PrimaryKeyCondition", reflect.TypeOf((*MockSessionRepository)(nil).PrimaryKeyCondition), arg0, arg1)
return &MockSessionRepositoryPrimaryKeyConditionCall{Call: call}
}
@@ -778,17 +854,17 @@ func (c *MockSessionRepositoryPrimaryKeyConditionCall) DoAndReturn(f func(string
}
// SetChallenge mocks base method.
func (m *MockSessionRepository) SetChallenge(challenge domain.SessionChallenge) database.Change {
func (m *MockSessionRepository) SetChallenge(arg0 domain.SessionChallenge) database.Change {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "SetChallenge", challenge)
ret := m.ctrl.Call(m, "SetChallenge", arg0)
ret0, _ := ret[0].(database.Change)
return ret0
}
// SetChallenge indicates an expected call of SetChallenge.
func (mr *MockSessionRepositoryMockRecorder) SetChallenge(challenge any) *MockSessionRepositorySetChallengeCall {
func (mr *MockSessionRepositoryMockRecorder) SetChallenge(arg0 any) *MockSessionRepositorySetChallengeCall {
mr.mock.ctrl.T.Helper()
call := mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "SetChallenge", reflect.TypeOf((*MockSessionRepository)(nil).SetChallenge), challenge)
call := mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "SetChallenge", reflect.TypeOf((*MockSessionRepository)(nil).SetChallenge), arg0)
return &MockSessionRepositorySetChallengeCall{Call: call}
}
@@ -816,17 +892,17 @@ func (c *MockSessionRepositorySetChallengeCall) DoAndReturn(f func(domain.Sessio
}
// SetFactor mocks base method.
func (m *MockSessionRepository) SetFactor(factor domain.SessionFactor) database.Change {
func (m *MockSessionRepository) SetFactor(arg0 domain.SessionFactor) database.Change {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "SetFactor", factor)
ret := m.ctrl.Call(m, "SetFactor", arg0)
ret0, _ := ret[0].(database.Change)
return ret0
}
// SetFactor indicates an expected call of SetFactor.
func (mr *MockSessionRepositoryMockRecorder) SetFactor(factor any) *MockSessionRepositorySetFactorCall {
func (mr *MockSessionRepositoryMockRecorder) SetFactor(arg0 any) *MockSessionRepositorySetFactorCall {
mr.mock.ctrl.T.Helper()
call := mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "SetFactor", reflect.TypeOf((*MockSessionRepository)(nil).SetFactor), factor)
call := mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "SetFactor", reflect.TypeOf((*MockSessionRepository)(nil).SetFactor), arg0)
return &MockSessionRepositorySetFactorCall{Call: call}
}
@@ -854,17 +930,17 @@ func (c *MockSessionRepositorySetFactorCall) DoAndReturn(f func(domain.SessionFa
}
// SetLifetime mocks base method.
func (m *MockSessionRepository) SetLifetime(lifetime time.Duration) database.Change {
func (m *MockSessionRepository) SetLifetime(arg0 time.Duration) database.Change {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "SetLifetime", lifetime)
ret := m.ctrl.Call(m, "SetLifetime", arg0)
ret0, _ := ret[0].(database.Change)
return ret0
}
// SetLifetime indicates an expected call of SetLifetime.
func (mr *MockSessionRepositoryMockRecorder) SetLifetime(lifetime any) *MockSessionRepositorySetLifetimeCall {
func (mr *MockSessionRepositoryMockRecorder) SetLifetime(arg0 any) *MockSessionRepositorySetLifetimeCall {
mr.mock.ctrl.T.Helper()
call := mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "SetLifetime", reflect.TypeOf((*MockSessionRepository)(nil).SetLifetime), lifetime)
call := mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "SetLifetime", reflect.TypeOf((*MockSessionRepository)(nil).SetLifetime), arg0)
return &MockSessionRepositorySetLifetimeCall{Call: call}
}
@@ -892,17 +968,17 @@ func (c *MockSessionRepositorySetLifetimeCall) DoAndReturn(f func(time.Duration)
}
// SetMetadata mocks base method.
func (m *MockSessionRepository) SetMetadata(metadata []domain.SessionMetadata) database.Change {
func (m *MockSessionRepository) SetMetadata(arg0 []*domain.SessionMetadata) database.Change {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "SetMetadata", metadata)
ret := m.ctrl.Call(m, "SetMetadata", arg0)
ret0, _ := ret[0].(database.Change)
return ret0
}
// SetMetadata indicates an expected call of SetMetadata.
func (mr *MockSessionRepositoryMockRecorder) SetMetadata(metadata any) *MockSessionRepositorySetMetadataCall {
func (mr *MockSessionRepositoryMockRecorder) SetMetadata(arg0 any) *MockSessionRepositorySetMetadataCall {
mr.mock.ctrl.T.Helper()
call := mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "SetMetadata", reflect.TypeOf((*MockSessionRepository)(nil).SetMetadata), metadata)
call := mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "SetMetadata", reflect.TypeOf((*MockSessionRepository)(nil).SetMetadata), arg0)
return &MockSessionRepositorySetMetadataCall{Call: call}
}
@@ -918,29 +994,29 @@ func (c *MockSessionRepositorySetMetadataCall) Return(arg0 database.Change) *Moc
}
// Do rewrite *gomock.Call.Do
func (c *MockSessionRepositorySetMetadataCall) Do(f func([]domain.SessionMetadata) database.Change) *MockSessionRepositorySetMetadataCall {
func (c *MockSessionRepositorySetMetadataCall) Do(f func([]*domain.SessionMetadata) database.Change) *MockSessionRepositorySetMetadataCall {
c.Call = c.Call.Do(f)
return c
}
// DoAndReturn rewrite *gomock.Call.DoAndReturn
func (c *MockSessionRepositorySetMetadataCall) DoAndReturn(f func([]domain.SessionMetadata) database.Change) *MockSessionRepositorySetMetadataCall {
func (c *MockSessionRepositorySetMetadataCall) DoAndReturn(f func([]*domain.SessionMetadata) database.Change) *MockSessionRepositorySetMetadataCall {
c.Call = c.Call.DoAndReturn(f)
return c
}
// SetToken mocks base method.
func (m *MockSessionRepository) SetToken(token string) database.Change {
func (m *MockSessionRepository) SetToken(arg0 string) database.Change {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "SetToken", token)
ret := m.ctrl.Call(m, "SetToken", arg0)
ret0, _ := ret[0].(database.Change)
return ret0
}
// SetToken indicates an expected call of SetToken.
func (mr *MockSessionRepositoryMockRecorder) SetToken(token any) *MockSessionRepositorySetTokenCall {
func (mr *MockSessionRepositoryMockRecorder) SetToken(arg0 any) *MockSessionRepositorySetTokenCall {
mr.mock.ctrl.T.Helper()
call := mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "SetToken", reflect.TypeOf((*MockSessionRepository)(nil).SetToken), token)
call := mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "SetToken", reflect.TypeOf((*MockSessionRepository)(nil).SetToken), arg0)
return &MockSessionRepositorySetTokenCall{Call: call}
}
@@ -968,17 +1044,17 @@ func (c *MockSessionRepositorySetTokenCall) DoAndReturn(f func(string) database.
}
// SetUpdatedAt mocks base method.
func (m *MockSessionRepository) SetUpdatedAt(updatedAt time.Time) database.Change {
func (m *MockSessionRepository) SetUpdatedAt(arg0 time.Time) database.Change {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "SetUpdatedAt", updatedAt)
ret := m.ctrl.Call(m, "SetUpdatedAt", arg0)
ret0, _ := ret[0].(database.Change)
return ret0
}
// SetUpdatedAt indicates an expected call of SetUpdatedAt.
func (mr *MockSessionRepositoryMockRecorder) SetUpdatedAt(updatedAt any) *MockSessionRepositorySetUpdatedAtCall {
func (mr *MockSessionRepositoryMockRecorder) SetUpdatedAt(arg0 any) *MockSessionRepositorySetUpdatedAtCall {
mr.mock.ctrl.T.Helper()
call := mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "SetUpdatedAt", reflect.TypeOf((*MockSessionRepository)(nil).SetUpdatedAt), updatedAt)
call := mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "SetUpdatedAt", reflect.TypeOf((*MockSessionRepository)(nil).SetUpdatedAt), arg0)
return &MockSessionRepositorySetUpdatedAtCall{Call: call}
}
@@ -1005,44 +1081,6 @@ func (c *MockSessionRepositorySetUpdatedAtCall) DoAndReturn(f func(time.Time) da
return c
}
// SetUserAgent mocks base method.
func (m *MockSessionRepository) SetUserAgent(userAgent domain.SessionUserAgent) database.Change {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "SetUserAgent", userAgent)
ret0, _ := ret[0].(database.Change)
return ret0
}
// SetUserAgent indicates an expected call of SetUserAgent.
func (mr *MockSessionRepositoryMockRecorder) SetUserAgent(userAgent any) *MockSessionRepositorySetUserAgentCall {
mr.mock.ctrl.T.Helper()
call := mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "SetUserAgent", reflect.TypeOf((*MockSessionRepository)(nil).SetUserAgent), userAgent)
return &MockSessionRepositorySetUserAgentCall{Call: call}
}
// MockSessionRepositorySetUserAgentCall wrap *gomock.Call
type MockSessionRepositorySetUserAgentCall struct {
*gomock.Call
}
// Return rewrite *gomock.Call.Return
func (c *MockSessionRepositorySetUserAgentCall) Return(arg0 database.Change) *MockSessionRepositorySetUserAgentCall {
c.Call = c.Call.Return(arg0)
return c
}
// Do rewrite *gomock.Call.Do
func (c *MockSessionRepositorySetUserAgentCall) Do(f func(domain.SessionUserAgent) database.Change) *MockSessionRepositorySetUserAgentCall {
c.Call = c.Call.Do(f)
return c
}
// DoAndReturn rewrite *gomock.Call.DoAndReturn
func (c *MockSessionRepositorySetUserAgentCall) DoAndReturn(f func(domain.SessionUserAgent) database.Change) *MockSessionRepositorySetUserAgentCall {
c.Call = c.Call.DoAndReturn(f)
return c
}
// TokenColumn mocks base method.
func (m *MockSessionRepository) TokenColumn() database.Column {
m.ctrl.T.Helper()
@@ -1082,10 +1120,10 @@ func (c *MockSessionRepositoryTokenColumnCall) DoAndReturn(f func() database.Col
}
// Update mocks base method.
func (m *MockSessionRepository) Update(ctx context.Context, client database.QueryExecutor, condition database.Condition, changes ...database.Change) (int64, error) {
func (m *MockSessionRepository) Update(arg0 context.Context, arg1 database.QueryExecutor, arg2 database.Condition, arg3 ...database.Change) (int64, error) {
m.ctrl.T.Helper()
varargs := []any{ctx, client, condition}
for _, a := range changes {
varargs := []any{arg0, arg1, arg2}
for _, a := range arg3 {
varargs = append(varargs, a)
}
ret := m.ctrl.Call(m, "Update", varargs...)
@@ -1095,9 +1133,9 @@ func (m *MockSessionRepository) Update(ctx context.Context, client database.Quer
}
// Update indicates an expected call of Update.
func (mr *MockSessionRepositoryMockRecorder) Update(ctx, client, condition any, changes ...any) *MockSessionRepositoryUpdateCall {
func (mr *MockSessionRepositoryMockRecorder) Update(arg0, arg1, arg2 any, arg3 ...any) *MockSessionRepositoryUpdateCall {
mr.mock.ctrl.T.Helper()
varargs := append([]any{ctx, client, condition}, changes...)
varargs := append([]any{arg0, arg1, arg2}, arg3...)
call := mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Update", reflect.TypeOf((*MockSessionRepository)(nil).Update), varargs...)
return &MockSessionRepositoryUpdateCall{Call: call}
}
@@ -1164,17 +1202,17 @@ func (c *MockSessionRepositoryUpdatedAtColumnCall) DoAndReturn(f func() database
}
// UpdatedAtCondition mocks base method.
func (m *MockSessionRepository) UpdatedAtCondition(op database.NumberOperation, updatedAt time.Time) database.Condition {
func (m *MockSessionRepository) UpdatedAtCondition(arg0 database.NumberOperation, arg1 time.Time) database.Condition {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "UpdatedAtCondition", op, updatedAt)
ret := m.ctrl.Call(m, "UpdatedAtCondition", arg0, arg1)
ret0, _ := ret[0].(database.Condition)
return ret0
}
// UpdatedAtCondition indicates an expected call of UpdatedAtCondition.
func (mr *MockSessionRepositoryMockRecorder) UpdatedAtCondition(op, updatedAt any) *MockSessionRepositoryUpdatedAtConditionCall {
func (mr *MockSessionRepositoryMockRecorder) UpdatedAtCondition(arg0, arg1 any) *MockSessionRepositoryUpdatedAtConditionCall {
mr.mock.ctrl.T.Helper()
call := mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "UpdatedAtCondition", reflect.TypeOf((*MockSessionRepository)(nil).UpdatedAtCondition), op, updatedAt)
call := mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "UpdatedAtCondition", reflect.TypeOf((*MockSessionRepository)(nil).UpdatedAtCondition), arg0, arg1)
return &MockSessionRepositoryUpdatedAtConditionCall{Call: call}
}
@@ -1201,94 +1239,18 @@ func (c *MockSessionRepositoryUpdatedAtConditionCall) DoAndReturn(f func(databas
return c
}
// UserAgentColumns mocks base method.
func (m *MockSessionRepository) UserAgentColumns() domain.SessionUserAgentColumns {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "UserAgentColumns")
ret0, _ := ret[0].(domain.SessionUserAgentColumns)
return ret0
}
// UserAgentColumns indicates an expected call of UserAgentColumns.
func (mr *MockSessionRepositoryMockRecorder) UserAgentColumns() *MockSessionRepositoryUserAgentColumnsCall {
mr.mock.ctrl.T.Helper()
call := mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "UserAgentColumns", reflect.TypeOf((*MockSessionRepository)(nil).UserAgentColumns))
return &MockSessionRepositoryUserAgentColumnsCall{Call: call}
}
// MockSessionRepositoryUserAgentColumnsCall wrap *gomock.Call
type MockSessionRepositoryUserAgentColumnsCall struct {
*gomock.Call
}
// Return rewrite *gomock.Call.Return
func (c *MockSessionRepositoryUserAgentColumnsCall) Return(arg0 domain.SessionUserAgentColumns) *MockSessionRepositoryUserAgentColumnsCall {
c.Call = c.Call.Return(arg0)
return c
}
// Do rewrite *gomock.Call.Do
func (c *MockSessionRepositoryUserAgentColumnsCall) Do(f func() domain.SessionUserAgentColumns) *MockSessionRepositoryUserAgentColumnsCall {
c.Call = c.Call.Do(f)
return c
}
// DoAndReturn rewrite *gomock.Call.DoAndReturn
func (c *MockSessionRepositoryUserAgentColumnsCall) DoAndReturn(f func() domain.SessionUserAgentColumns) *MockSessionRepositoryUserAgentColumnsCall {
c.Call = c.Call.DoAndReturn(f)
return c
}
// UserAgentConditions mocks base method.
func (m *MockSessionRepository) UserAgentConditions() domain.SessionUserAgentConditions {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "UserAgentConditions")
ret0, _ := ret[0].(domain.SessionUserAgentConditions)
return ret0
}
// UserAgentConditions indicates an expected call of UserAgentConditions.
func (mr *MockSessionRepositoryMockRecorder) UserAgentConditions() *MockSessionRepositoryUserAgentConditionsCall {
mr.mock.ctrl.T.Helper()
call := mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "UserAgentConditions", reflect.TypeOf((*MockSessionRepository)(nil).UserAgentConditions))
return &MockSessionRepositoryUserAgentConditionsCall{Call: call}
}
// MockSessionRepositoryUserAgentConditionsCall wrap *gomock.Call
type MockSessionRepositoryUserAgentConditionsCall struct {
*gomock.Call
}
// Return rewrite *gomock.Call.Return
func (c *MockSessionRepositoryUserAgentConditionsCall) Return(arg0 domain.SessionUserAgentConditions) *MockSessionRepositoryUserAgentConditionsCall {
c.Call = c.Call.Return(arg0)
return c
}
// Do rewrite *gomock.Call.Do
func (c *MockSessionRepositoryUserAgentConditionsCall) Do(f func() domain.SessionUserAgentConditions) *MockSessionRepositoryUserAgentConditionsCall {
c.Call = c.Call.Do(f)
return c
}
// DoAndReturn rewrite *gomock.Call.DoAndReturn
func (c *MockSessionRepositoryUserAgentConditionsCall) DoAndReturn(f func() domain.SessionUserAgentConditions) *MockSessionRepositoryUserAgentConditionsCall {
c.Call = c.Call.DoAndReturn(f)
return c
}
// UserAgentIDCondition mocks base method.
func (m *MockSessionRepository) UserAgentIDCondition(userAgentID string) database.Condition {
func (m *MockSessionRepository) UserAgentIDCondition(arg0 string) database.Condition {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "UserAgentIDCondition", userAgentID)
ret := m.ctrl.Call(m, "UserAgentIDCondition", arg0)
ret0, _ := ret[0].(database.Condition)
return ret0
}
// UserAgentIDCondition indicates an expected call of UserAgentIDCondition.
func (mr *MockSessionRepositoryMockRecorder) UserAgentIDCondition(userAgentID any) *MockSessionRepositoryUserAgentIDConditionCall {
func (mr *MockSessionRepositoryMockRecorder) UserAgentIDCondition(arg0 any) *MockSessionRepositoryUserAgentIDConditionCall {
mr.mock.ctrl.T.Helper()
call := mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "UserAgentIDCondition", reflect.TypeOf((*MockSessionRepository)(nil).UserAgentIDCondition), userAgentID)
call := mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "UserAgentIDCondition", reflect.TypeOf((*MockSessionRepository)(nil).UserAgentIDCondition), arg0)
return &MockSessionRepositoryUserAgentIDConditionCall{Call: call}
}
@@ -1354,17 +1316,17 @@ func (c *MockSessionRepositoryUserIDColumnCall) DoAndReturn(f func() database.Co
}
// UserIDCondition mocks base method.
func (m *MockSessionRepository) UserIDCondition(userID string) database.Condition {
func (m *MockSessionRepository) UserIDCondition(arg0 string) database.Condition {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "UserIDCondition", userID)
ret := m.ctrl.Call(m, "UserIDCondition", arg0)
ret0, _ := ret[0].(database.Condition)
return ret0
}
// UserIDCondition indicates an expected call of UserIDCondition.
func (mr *MockSessionRepositoryMockRecorder) UserIDCondition(userID any) *MockSessionRepositoryUserIDConditionCall {
func (mr *MockSessionRepositoryMockRecorder) UserIDCondition(arg0 any) *MockSessionRepositoryUserIDConditionCall {
mr.mock.ctrl.T.Helper()
call := mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "UserIDCondition", reflect.TypeOf((*MockSessionRepository)(nil).UserIDCondition), userID)
call := mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "UserIDCondition", reflect.TypeOf((*MockSessionRepository)(nil).UserIDCondition), arg0)
return &MockSessionRepositoryUserIDConditionCall{Call: call}
}
+27 -30
View File
@@ -8,18 +8,19 @@ import (
)
type Session struct {
InstanceID string `json:"instance_id,omitempty" db:"instance_id"`
ID string `json:"id,omitempty" db:"id"`
Token string `json:"token,omitempty" db:"token"`
Lifetime time.Duration `json:"lifetime,omitempty" db:"lifetime"`
Expiration time.Time `json:"expiration,omitzero" db:"expiration"`
UserID string `json:"user_id,omitempty" db:"user_id"`
CreatedAt time.Time `json:"created_at,omitzero" db:"created_at"`
UpdatedAt time.Time `json:"updated_at,omitzero" db:"updated_at"`
Factors SessionFactors `json:"factors,omitempty"`
Challenges SessionChallenges `json:"challenges,omitempty"`
Metadata []SessionMetadata `json:"metadata,omitempty"`
UserAgent *SessionUserAgent `json:"user_agent,omitempty"`
InstanceID string `json:"instanceId,omitempty" db:"instance_id"`
ID string `json:"id,omitempty" db:"id"`
TokenID string `json:"tokenId,omitempty" db:"token_id"`
Lifetime time.Duration `json:"lifetime,omitempty" db:"lifetime"`
Expiration time.Time `json:"expiration,omitzero" db:"expiration"`
UserID string `json:"userId,omitempty" db:"user_id"`
CreatorID string `json:"creatorId,omitempty" db:"creator_id"`
CreatedAt time.Time `json:"createdAt,omitzero" db:"created_at"`
UpdatedAt time.Time `json:"updatedAt,omitzero" db:"updated_at"`
Factors SessionFactors `json:"factors,omitempty"`
Challenges SessionChallenges `json:"challenges,omitempty"`
Metadata []*SessionMetadata `json:"metadata,omitempty"`
UserAgent *SessionUserAgent `json:"userAgent,omitempty"`
}
//go:generate mockgen -typed -package domainmock -destination ./mock/session.mock.go . SessionRepository
@@ -41,7 +42,7 @@ type SessionRepository interface {
// The condition must include at least the instanceID of the session to update.
Update(ctx context.Context, client database.QueryExecutor, condition database.Condition, changes ...database.Change) (int64, error)
// Delete removes sessions based on the given condition.
Delete(ctx context.Context, client database.QueryExecutor, condition database.Condition) error
Delete(ctx context.Context, client database.QueryExecutor, condition database.Condition) (int64, error)
}
// sessionColumns define all the columns of the session table.
@@ -50,8 +51,8 @@ type sessionColumns interface {
InstanceIDColumn() database.Column
// IDColumn returns the column for the id field.
IDColumn() database.Column
// TokenColumn returns the column for the token field.
TokenColumn() database.Column
// TokenIDColumn returns the column for the token id field.
TokenIDColumn() database.Column
// LifetimeColumn returns the column for the lifetime field.
LifetimeColumn() database.Column
// ExpirationColumn returns the column for the expiration field.
@@ -62,13 +63,6 @@ type sessionColumns interface {
CreatedAtColumn() database.Column
// UpdatedAtColumn returns the column for the updated at field.
UpdatedAtColumn() database.Column
// FactorColumns returns the columns for the factors fields.
FactorColumns() SessionFactorColumns
// MetadataColumns returns the columns for the metadata fields.
MetadataColumns() SessionMetadataColumns
// UserAgentColumns returns the columns for the user agent fields.
UserAgentColumns() SessionUserAgentColumns
}
// sessionConditions define all the conditions for the session table.
@@ -83,17 +77,22 @@ type sessionConditions interface {
UserAgentIDCondition(userAgentID string) database.Condition
// UserIDCondition returns an equal filter on the user id field.
UserIDCondition(userID string) database.Condition
// CreatorIDCondition returns an equal filter on the creator id field.
CreatorIDCondition(creatorID string) database.Condition
// ExpirationCondition returns a filter on the expiration field.
ExpirationCondition(op database.NumberOperation, expiration time.Time) database.Condition
// CreatedAtCondition returns a filter on the created at field.
CreatedAtCondition(op database.NumberOperation, createdAt time.Time) database.Condition
// UpdatedAtCondition returns a filter on the updated at field.
UpdatedAtCondition(op database.NumberOperation, updatedAt time.Time) database.Condition
// ExistsFactor returns a filter on the session's factors.
ExistsFactor(condition database.Condition) database.Condition
// FactorConditions returns the conditions for the factors fields.
FactorConditions() SessionFactorConditions
// ExistsMetadata returns a filter on the session's metadata.
ExistsMetadata(condition database.Condition) database.Condition
// MetadataConditions returns the conditions for the metadata fields.
MetadataConditions() SessionMetadataConditions
// UserAgentConditions returns the conditions for the user agent fields.
UserAgentConditions() SessionUserAgentConditions
}
type sessionChanges interface {
@@ -101,7 +100,7 @@ type sessionChanges interface {
// Only use this when reducing events,
// during regular updates the DB sets this column automatically.
SetUpdatedAt(updatedAt time.Time) database.Change
// SetToken sets the token field of the session.
// SetToken sets the token id field of the session.
SetToken(token string) database.Change
// SetLifetime sets the lifetime field of the session and will update the computed expiration field.
SetLifetime(lifetime time.Duration) database.Change
@@ -110,10 +109,8 @@ type sessionChanges interface {
SetChallenge(challenge SessionChallenge) database.Change
// SetFactor adds or updates the factor of the corresponding type.
SetFactor(factor SessionFactor) database.Change
// ClearFactor resets the factor of the corresponding type.
// ClearFactor resets the factor's verification.
ClearFactor(factor SessionFactorType) database.Change
// SetMetadata adds or updates the metadata of the session.
SetMetadata(metadata []SessionMetadata) database.Change
// SetUserAgent adds or updates the user agent of the session.
SetUserAgent(userAgent SessionUserAgent) database.Change
SetMetadata(metadata []*SessionMetadata) database.Change
}
+85 -78
View File
@@ -8,94 +8,112 @@ import (
"github.com/zitadel/zitadel/internal/domain"
)
type SessionFactorType int
//go:generate enumer -type=SessionFactorType -trimprefix SessionFactorType -transform snake -linecomment -sql
type SessionFactorType uint8
const (
SessionFactorTypeUnknown SessionFactorType = iota
SessionFactorTypeUser
SessionFactorTypePassword
SessionFactorTypePasskey // formerly known as WebAuthn
// SessionFactorTypePasskey was formerly known as WebAuthn
SessionFactorTypePasskey
SessionFactorTypeIdentityProviderIntent
// SessionFactorTypeTOTP used to be called OTP in some places
SessionFactorTypeTOTP
SessionFactorTypeOTPSMS
SessionFactorTypeOTPSMS // otp_sms
SessionFactorTypeOTPEmail
SessionFactorTypeRecoveryCode
)
type SessionFactor interface {
sessionFactorType() SessionFactorType
SessionFactorType() SessionFactorType
}
type SessionFactorUser struct {
UserID string
LastVerifiedAt time.Time
UserID string `json:"userId,omitempty" db:"user_id"`
LastVerifiedAt time.Time `json:"lastVerifiedAt,omitzero" db:"last_verified_at"`
}
// sessionFactorType implements [SessionFactor].
func (s *SessionFactorUser) sessionFactorType() SessionFactorType {
// SessionFactorType implements [SessionFactor].
func (s *SessionFactorUser) SessionFactorType() SessionFactorType {
return SessionFactorTypeUser
}
type SessionFactorPassword struct {
LastVerifiedAt time.Time
LastFailedAt time.Time
LastVerifiedAt time.Time `json:"lastVerifiedAt,omitzero" db:"last_verified_at"`
LastFailedAt time.Time `json:"lastFailedAt,omitzero" db:"last_failed_at"`
}
// sessionFactorType implements [SessionFactor].
func (s *SessionFactorPassword) sessionFactorType() SessionFactorType {
// SessionFactorType implements [SessionFactor].
func (s *SessionFactorPassword) SessionFactorType() SessionFactorType {
return SessionFactorTypePassword
}
type SessionFactorIdentityProviderIntent struct {
LastVerifiedAt time.Time
LastVerifiedAt time.Time `json:"lastVerifiedAt,omitzero" db:"last_verified_at"`
}
// sessionFactorType implements [SessionFactor].
func (s *SessionFactorIdentityProviderIntent) sessionFactorType() SessionFactorType {
// SessionFactorType implements [SessionFactor].
func (s *SessionFactorIdentityProviderIntent) SessionFactorType() SessionFactorType {
return SessionFactorTypeIdentityProviderIntent
}
type SessionFactorPasskey struct {
LastVerifiedAt time.Time
UserVerified bool
LastVerifiedAt time.Time `json:"lastVerifiedAt,omitzero" db:"last_verified_at"`
UserVerified bool `json:"userVerified,omitempty" db:"user_verified"`
}
// sessionFactorType implements [SessionFactor].
func (s *SessionFactorPasskey) sessionFactorType() SessionFactorType {
// SessionFactorType implements [SessionFactor].
func (s *SessionFactorPasskey) SessionFactorType() SessionFactorType {
return SessionFactorTypePasskey
}
type SessionFactorTOTP struct {
LastVerifiedAt time.Time
LastFailedAt time.Time
LastVerifiedAt time.Time `json:"lastVerifiedAt,omitzero" db:"last_verified_at"`
LastFailedAt time.Time `json:"lastFailedAt,omitzero" db:"last_failed_at"`
}
// sessionFactorType implements [SessionFactor].
func (s *SessionFactorTOTP) sessionFactorType() SessionFactorType {
// SessionFactorType implements [SessionFactor].
func (s *SessionFactorTOTP) SessionFactorType() SessionFactorType {
return SessionFactorTypeTOTP
}
type SessionFactorOTPSMS struct {
LastVerifiedAt time.Time
LastFailedAt time.Time
LastVerifiedAt time.Time `json:"lastVerifiedAt,omitzero" db:"last_verified_at"`
LastFailedAt time.Time `json:"lastFailedAt,omitzero" db:"last_failed_at"`
}
// sessionFactorType implements [SessionFactor].
func (s *SessionFactorOTPSMS) sessionFactorType() SessionFactorType {
// SessionFactorType implements [SessionFactor].
func (s *SessionFactorOTPSMS) SessionFactorType() SessionFactorType {
return SessionFactorTypeOTPSMS
}
type SessionFactorOTPEmail struct {
LastVerifiedAt time.Time
LastFailedAt time.Time
LastVerifiedAt time.Time `json:"lastVerifiedAt,omitzero" db:"last_verified_at"`
LastFailedAt time.Time `json:"lastFailedAt,omitzero" db:"last_failed_at"`
}
// sessionFactorType implements [SessionFactor].
func (s *SessionFactorOTPEmail) sessionFactorType() SessionFactorType {
// SessionFactorType implements [SessionFactor].
func (s *SessionFactorOTPEmail) SessionFactorType() SessionFactorType {
return SessionFactorTypeOTPEmail
}
type SessionFactorRecoveryCode struct {
LastVerifiedAt time.Time `json:"lastVerifiedAt,omitzero" db:"last_verified_at"`
LastFailedAt time.Time `json:"lastFailedAt,omitzero" db:"last_failed_at"`
}
// SessionFactorType implements [SessionFactor].
func (s *SessionFactorRecoveryCode) SessionFactorType() SessionFactorType {
return SessionFactorTypeRecoveryCode
}
type SessionFactors []SessionFactor
func (s *SessionFactors) AppendTo(factor SessionFactor) {
*s = append(*s, factor)
}
// GetUserFactor returns the user factor from the factors.
func (s SessionFactors) GetUserFactor() *SessionFactorUser {
factor, _ := GetFactor[*SessionFactorUser](s)
@@ -138,6 +156,12 @@ func (s SessionFactors) GetOTPEmailFactor() *SessionFactorOTPEmail {
return factor
}
// GetRecoveryCodeFactor returns the recovery code factor from the factors.
func (s SessionFactors) GetRecoveryCodeFactor() *SessionFactorRecoveryCode {
factor, _ := GetFactor[*SessionFactorRecoveryCode](s)
return factor
}
// GetFactor returns the factor of the given type from the factors.
func GetFactor[T SessionFactor](factors SessionFactors) (T, bool) {
var nilT T
@@ -150,52 +174,56 @@ func GetFactor[T SessionFactor](factors SessionFactors) (T, bool) {
}
type SessionChallenge interface {
sessionChallengeType() SessionFactorType
SessionChallengeType() SessionFactorType
}
type SessionChallengePasskey struct {
LastChallengedAt time.Time
Challenge string
AllowedCredentialIDs [][]byte
UserVerification domain.UserVerificationRequirement
RPID string
LastChallengedAt time.Time `json:"lastChallengedAt,omitzero" db:"last_challenged_at"`
Challenge string `json:"challenge,omitempty" db:"challenge"`
AllowedCredentialIDs [][]byte `json:"allowedCredentialIDs,omitempty" db:"allowed_credential_ids"`
UserVerification domain.UserVerificationRequirement `json:"userVerification,omitempty" db:"user_verification"`
RPID string `json:"rpId,omitempty" db:"rp_id"`
}
// sessionChallengeType implements [SessionChallenge].
func (s *SessionChallengePasskey) sessionChallengeType() SessionFactorType {
// SessionChallengeType implements [SessionChallenge].
func (s *SessionChallengePasskey) SessionChallengeType() SessionFactorType {
return SessionFactorTypePasskey
}
type SessionChallengeOTPSMS struct {
LastChallengedAt time.Time
Code *crypto.CryptoValue
Expiry time.Duration
CodeReturned bool
GeneratorID string
TriggeredAtOrigin string
LastChallengedAt time.Time `json:"lastChallengedAt,omitzero" db:"last_challenged_at"`
Code *crypto.CryptoValue `json:"code,omitempty" db:"code"`
Expiry time.Duration `json:"expiry,omitzero" db:"expiry"`
CodeReturned bool `json:"codeReturned,omitempty" db:"code_returned"`
GeneratorID string `json:"generatorId,omitempty" db:"generator_id"`
TriggeredAtOrigin string `json:"triggeredAtOrigin,omitempty" db:"triggered_at_origin"`
}
// sessionChallengeType implements [SessionChallenge].
func (s *SessionChallengeOTPSMS) sessionChallengeType() SessionFactorType {
// SessionChallengeType implements [SessionChallenge].
func (s *SessionChallengeOTPSMS) SessionChallengeType() SessionFactorType {
return SessionFactorTypeOTPSMS
}
type SessionChallengeOTPEmail struct {
LastChallengedAt time.Time
Code *crypto.CryptoValue
Expiry time.Duration
CodeReturned bool
URLTmpl string
TriggeredAtOrigin string
LastChallengedAt time.Time `json:"lastChallengedAt,omitzero" db:"last_challenged_at"`
Code *crypto.CryptoValue `json:"code,omitempty" db:"code"`
Expiry time.Duration `json:"expiry,omitzero" db:"expiry"`
CodeReturned bool `json:"codeReturned,omitempty" db:"code_returned"`
URLTemplate string `json:"urlTemplate,omitempty" db:"url_template"`
TriggeredAtOrigin string `json:"triggeredAtOrigin,omitempty" db:"triggered_at_origin"`
}
// sessionChallengeType implements [SessionChallenge].
func (s *SessionChallengeOTPEmail) sessionChallengeType() SessionFactorType {
// SessionChallengeType implements [SessionChallenge].
func (s *SessionChallengeOTPEmail) SessionChallengeType() SessionFactorType {
return SessionFactorTypeOTPEmail
}
type SessionChallenges []SessionChallenge
func (s *SessionChallenges) AppendTo(challenge SessionChallenge) {
*s = append(*s, challenge)
}
// GetPasskeyChallenge returns the passkey challenge from the challenges.
func (s SessionChallenges) GetPasskeyChallenge() *SessionChallengePasskey {
challenge, _ := GetChallenge[*SessionChallengePasskey](s)
@@ -225,30 +253,9 @@ func GetChallenge[T SessionChallenge](challenges SessionChallenges) (T, bool) {
return nilT, false
}
type SessionFactorColumns interface {
// InstanceIDColumn returns the column for the instance id field.
InstanceIDColumn() database.Column
// SessionIDColumn returns the column for the session id field.
SessionIDColumn() database.Column
// FactorTypeColumn returns the column for the factor type field.
FactorTypeColumn() database.Column
// LastChallengedAtColumn returns the column for the last challenged at field.
LastChallengedAtColumn() database.Column
// LastFailedAtColumn returns the column for the last failed at field.
LastFailedAtColumn() database.Column
// LastVerifiedAtColumn returns the column for the last verified at field.
LastVerifiedAtColumn() database.Column
}
type SessionFactorConditions interface {
// PrimaryKeyCondition returns a filter on the primary key fields.
PrimaryKeyCondition(instanceID, sessionID string, factorType SessionFactorType) database.Condition
// InstanceIDCondition returns an equal filter on the instance id field.
InstanceIDCondition(instanceID string) database.Condition
// SessionIDCondition returns an equal filter on the session id field.
SessionIDCondition(sessionID string) database.Condition
// FactorTypeCondition returns an equal filter on the factor type field.
FactorTypeCondition(factorType SessionFactorType) database.Condition
// FactorLastVerifiedBeforeCondition returns a filter on the factor last verified field before the given time.
FactorLastVerifiedBeforeCondition(lastVerifiedAt time.Time) database.Condition
// LastVerifiedBeforeCondition returns a filter on the factor last verified field before the given time.
LastVerifiedBeforeCondition(lastVerifiedAt time.Time) database.Condition
}
+1 -2
View File
@@ -8,11 +8,10 @@ import (
)
type SessionUserAgent struct {
InstanceID string `json:"instanceId,omitempty" db:"instance_id"`
FingerprintID *string `json:"fingerprintId,omitempty" db:"fingerprint_id"`
Description *string `json:"description,omitempty" db:"description"`
IP net.IP `json:"ip,omitempty" db:"ip"`
Header http.Header `json:"header,omitempty" db:"headers"`
Header http.Header `json:"headers,omitempty" db:"headers"`
}
type SessionUserAgentColumns interface {
@@ -0,0 +1,137 @@
// Code generated by "enumer -type=SessionFactorType -trimprefix SessionFactorType -transform snake -linecomment -sql"; DO NOT EDIT.
package domain
import (
"database/sql/driver"
"fmt"
"strings"
)
const _SessionFactorTypeName = "unknownuserpasswordpasskeyidentity_provider_intenttotpotp_smsotp_emailrecovery_code"
var _SessionFactorTypeIndex = [...]uint8{0, 7, 11, 19, 26, 50, 54, 61, 70, 83}
const _SessionFactorTypeLowerName = "unknownuserpasswordpasskeyidentity_provider_intenttotpotp_smsotp_emailrecovery_code"
func (i SessionFactorType) String() string {
if i >= SessionFactorType(len(_SessionFactorTypeIndex)-1) {
return fmt.Sprintf("SessionFactorType(%d)", i)
}
return _SessionFactorTypeName[_SessionFactorTypeIndex[i]:_SessionFactorTypeIndex[i+1]]
}
// An "invalid array index" compiler error signifies that the constant values have changed.
// Re-run the stringer command to generate them again.
func _SessionFactorTypeNoOp() {
var x [1]struct{}
_ = x[SessionFactorTypeUnknown-(0)]
_ = x[SessionFactorTypeUser-(1)]
_ = x[SessionFactorTypePassword-(2)]
_ = x[SessionFactorTypePasskey-(3)]
_ = x[SessionFactorTypeIdentityProviderIntent-(4)]
_ = x[SessionFactorTypeTOTP-(5)]
_ = x[SessionFactorTypeOTPSMS-(6)]
_ = x[SessionFactorTypeOTPEmail-(7)]
_ = x[SessionFactorTypeRecoveryCode-(8)]
}
var _SessionFactorTypeValues = []SessionFactorType{SessionFactorTypeUnknown, SessionFactorTypeUser, SessionFactorTypePassword, SessionFactorTypePasskey, SessionFactorTypeIdentityProviderIntent, SessionFactorTypeTOTP, SessionFactorTypeOTPSMS, SessionFactorTypeOTPEmail, SessionFactorTypeRecoveryCode}
var _SessionFactorTypeNameToValueMap = map[string]SessionFactorType{
_SessionFactorTypeName[0:7]: SessionFactorTypeUnknown,
_SessionFactorTypeLowerName[0:7]: SessionFactorTypeUnknown,
_SessionFactorTypeName[7:11]: SessionFactorTypeUser,
_SessionFactorTypeLowerName[7:11]: SessionFactorTypeUser,
_SessionFactorTypeName[11:19]: SessionFactorTypePassword,
_SessionFactorTypeLowerName[11:19]: SessionFactorTypePassword,
_SessionFactorTypeName[19:26]: SessionFactorTypePasskey,
_SessionFactorTypeLowerName[19:26]: SessionFactorTypePasskey,
_SessionFactorTypeName[26:50]: SessionFactorTypeIdentityProviderIntent,
_SessionFactorTypeLowerName[26:50]: SessionFactorTypeIdentityProviderIntent,
_SessionFactorTypeName[50:54]: SessionFactorTypeTOTP,
_SessionFactorTypeLowerName[50:54]: SessionFactorTypeTOTP,
_SessionFactorTypeName[54:61]: SessionFactorTypeOTPSMS,
_SessionFactorTypeLowerName[54:61]: SessionFactorTypeOTPSMS,
_SessionFactorTypeName[61:70]: SessionFactorTypeOTPEmail,
_SessionFactorTypeLowerName[61:70]: SessionFactorTypeOTPEmail,
_SessionFactorTypeName[70:83]: SessionFactorTypeRecoveryCode,
_SessionFactorTypeLowerName[70:83]: SessionFactorTypeRecoveryCode,
}
var _SessionFactorTypeNames = []string{
_SessionFactorTypeName[0:7],
_SessionFactorTypeName[7:11],
_SessionFactorTypeName[11:19],
_SessionFactorTypeName[19:26],
_SessionFactorTypeName[26:50],
_SessionFactorTypeName[50:54],
_SessionFactorTypeName[54:61],
_SessionFactorTypeName[61:70],
_SessionFactorTypeName[70:83],
}
// SessionFactorTypeString retrieves an enum value from the enum constants string name.
// Throws an error if the param is not part of the enum.
func SessionFactorTypeString(s string) (SessionFactorType, error) {
if val, ok := _SessionFactorTypeNameToValueMap[s]; ok {
return val, nil
}
if val, ok := _SessionFactorTypeNameToValueMap[strings.ToLower(s)]; ok {
return val, nil
}
return 0, fmt.Errorf("%s does not belong to SessionFactorType values", s)
}
// SessionFactorTypeValues returns all values of the enum
func SessionFactorTypeValues() []SessionFactorType {
return _SessionFactorTypeValues
}
// SessionFactorTypeStrings returns a slice of all String values of the enum
func SessionFactorTypeStrings() []string {
strs := make([]string, len(_SessionFactorTypeNames))
copy(strs, _SessionFactorTypeNames)
return strs
}
// IsASessionFactorType returns "true" if the value is listed in the enum definition. "false" otherwise
func (i SessionFactorType) IsASessionFactorType() bool {
for _, v := range _SessionFactorTypeValues {
if i == v {
return true
}
}
return false
}
func (i SessionFactorType) Value() (driver.Value, error) {
return i.String(), nil
}
func (i *SessionFactorType) Scan(value interface{}) error {
if value == nil {
return nil
}
var str string
switch v := value.(type) {
case []byte:
str = string(v)
case string:
str = v
case fmt.Stringer:
str = v.String()
default:
return fmt.Errorf("invalid value of SessionFactorType: %[1]T(%[1]v)", value)
}
val, err := SessionFactorTypeString(str)
if err != nil {
return err
}
*i = val
return nil
}
+41 -6
View File
@@ -17,6 +17,12 @@ type Change interface {
IsOnColumn(col Column) bool
}
// NoChange can be implemented by Changes that might not result in any actual change,
// resp. when the change has no set statement, e.g. [CTEChange] with no change function.
type NoChange interface {
NoChange() bool
}
type change[V Value] struct {
column Column
value V
@@ -88,15 +94,23 @@ func (c Changes) IsOnColumn(col Column) bool {
}
// Write implements [Change].
func (m Changes) Write(builder *StatementBuilder) error {
for i, change := range m {
if i > 0 {
func (c Changes) Write(builder *StatementBuilder) error {
var hadChanges bool
for _, change := range c {
ch, ok := change.(NoChange)
hasChanges := !ok || !ch.NoChange()
// if the previous change actually wrote a change to the builder
// and this change has changes, add a comma
if hadChanges && hasChanges {
builder.WriteString(", ")
}
err := change.Write(builder)
if err != nil {
if err := change.Write(builder); err != nil {
return err
}
// if the change had changes, mark that we had changes for the next iteration
if hasChanges {
hadChanges = hasChanges
}
}
return nil
}
@@ -123,6 +137,21 @@ func (c Changes) String() string {
return "database.Changes"
}
// NoChange implements [NoChange].
// It returns false if any of the changes writes to the builder.
func (c Changes) NoChange() bool {
for _, change := range c {
no, ok := change.(NoChange)
if !ok {
return false
}
if no.NoChange() {
return true
}
}
return true
}
var _ Change = Changes(nil)
func NewChangeToNull(col Column) Change {
@@ -319,7 +348,7 @@ func (c *cteChange) Matches(x any) bool {
return slices.Equal(expectedCTEBuilder.Args(), actualCTEBuilder.Args())
}
// Name implements [CTEChange].
// SetName implements [CTEChange].
func (c *cteChange) SetName(name string) {
c.name = name
}
@@ -342,3 +371,9 @@ func (c *cteChange) Write(builder *StatementBuilder) error {
func (c *cteChange) WriteCTE(builder *StatementBuilder) {
c.cte(builder)
}
// NoChange implements [NoChange].
// It returns true if there is no change function defined.
func (c *cteChange) NoChange() bool {
return c.change == nil
}
@@ -1,3 +1,3 @@
DROP TYPE zitadel.project_grant_state;
DROP TYPE IF EXISTS zitadel.project_grant_state CASCADE ;
DROP TABLE IF EXISTS zitadel.project_grant_roles;
DROP TABLE IF EXISTS zitadel.project_grants;
@@ -0,0 +1,16 @@
package migration
import (
_ "embed"
)
var (
//go:embed 011_session_table/up.sql
up011SessionTable string
//go:embed 011_session_table/down.sql
down011SessionTable string
)
func init() {
registerSQLMigration(11, up011SessionTable, down011SessionTable)
}
@@ -0,0 +1,6 @@
DROP TABLE IF EXISTS zitadel.session_metadata CASCADE;
DROP TABLE IF EXISTS zitadel.session_user_agents CASCADE;
DROP TYPE IF EXISTS zitadel.session_factor_type CASCADE;
DROP TABLE IF EXISTS zitadel.session_factors CASCADE;
DROP TABLE IF EXISTS zitadel.sessions CASCADE;
DROP FUNCTION IF EXISTS zitadel.update_expiration();
@@ -0,0 +1,93 @@
CREATE TABLE zitadel.session_user_agents (
instance_id TEXT NOT NULL
, fingerprint_id TEXT NOT NULL CHECK (fingerprint_id <> '')
, ip INET
, description TEXT
, headers JSONB
, PRIMARY KEY (instance_id, fingerprint_id)
);
CREATE TABLE zitadel.sessions (
instance_id TEXT NOT NULL
, id TEXT NOT NULL CHECK (id <> '')
, token_id TEXT
, user_agent_id TEXT
, lifetime INTERVAL
, expiration TIMESTAMPTZ
, user_id TEXT -- this column is used for referential integrity
, creator_id TEXT
, created_at TIMESTAMPTZ DEFAULT NOW() NOT NULL
, updated_at TIMESTAMPTZ DEFAULT NOW() NOT NULL
, PRIMARY KEY (instance_id, id)
, FOREIGN KEY (instance_id) REFERENCES zitadel.instances(id)
-- , FOREIGN KEY (instance_id, user_id) REFERENCES zitadel.users(instance_id, id) ON DELETE CASCADE
, FOREIGN KEY (instance_id, user_agent_id) REFERENCES zitadel.session_user_agents(instance_id, fingerprint_id) ON DELETE SET NULL (user_agent_id)
);
CREATE OR REPLACE FUNCTION zitadel.update_expiration()
RETURNS TRIGGER AS $$
BEGIN
NEW.expiration := NEW.updated_at + NEW.lifetime;
RETURN NEW;
END;
$$ LANGUAGE plpgsql;
CREATE TRIGGER set_expiration
BEFORE INSERT OR UPDATE OF lifetime ON zitadel.sessions
FOR EACH ROW
EXECUTE FUNCTION zitadel.update_expiration();
CREATE TYPE zitadel.session_factor_type AS ENUM (
'user',
'password',
'passkey', -- is also a challenge
'identity_provider_intent',
'totp',
'otp_sms', -- is also a challenge
'otp_email', -- is also a challenge
'recovery_code'
);
CREATE TABLE zitadel.session_factors (
instance_id TEXT NOT NULL
, session_id TEXT NOT NULL
, type zitadel.session_factor_type NOT NULL
, last_challenged_at TIMESTAMPTZ -- this is only set if the type is a challenge
, challenged_payload JSONB
, last_verified_at TIMESTAMPTZ
, verified_payload JSONB
, PRIMARY KEY (instance_id, session_id, type)
, FOREIGN KEY (instance_id, session_id) REFERENCES zitadel.sessions(instance_id, id) ON DELETE CASCADE
);
CREATE TABLE zitadel.session_metadata (
instance_id TEXT NOT NULL
, session_id TEXT NOT NULL
, key TEXT NOT NULL CHECK (key <> '')
, value BYTEA NOT NULL
, created_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
, updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
, PRIMARY KEY (instance_id, session_id, key)
, CONSTRAINT fk_session_metadata_session FOREIGN KEY (instance_id, session_id) REFERENCES zitadel.sessions (instance_id, id) ON DELETE CASCADE
);
CREATE INDEX idx_session_metadata_key ON zitadel.session_metadata (key);
CREATE INDEX idx_session_metadata_value ON zitadel.session_metadata (sha256(value));
-- TODO(adlerhurst): these indexes can currently not be used by Postgres, because of the type conversion
-- the value can be a json but doesn't have to be.
-- CREATE INDEX idx_session_metadata_value_number ON zitadel.session_metadata ((value::NUMERIC)) WHERE jsonb_typeof(value) = 'number';
-- CREATE INDEX idx_session_metadata_value_string ON zitadel.session_metadata ((value#>>'{}')) WHERE jsonb_typeof(value) = 'string';
-- CREATE INDEX idx_session_metadata_value_boolean ON zitadel.session_metadata ((value::BOOLEAN)) WHERE jsonb_typeof(value) = 'boolean';
CREATE TRIGGER trg_set_updated_at_session_metadata
BEFORE INSERT OR UPDATE ON zitadel.session_metadata
FOR EACH ROW
WHEN (NEW.updated_at IS NULL)
EXECUTE FUNCTION zitadel.set_updated_at();
@@ -22,8 +22,9 @@ import (
mgmt "github.com/zitadel/zitadel/pkg/grpc/management"
v2beta_org "github.com/zitadel/zitadel/pkg/grpc/org/v2beta"
v2beta_project "github.com/zitadel/zitadel/pkg/grpc/project/v2beta"
"github.com/zitadel/zitadel/pkg/grpc/session/v2"
"github.com/zitadel/zitadel/pkg/grpc/system"
user_v2 "github.com/zitadel/zitadel/pkg/grpc/user/v2"
"github.com/zitadel/zitadel/pkg/grpc/user/v2"
)
func getEnv(key, fallback string) string {
@@ -51,9 +52,10 @@ var (
SystemClient system.SystemServiceClient
OrgClient v2beta_org.OrganizationServiceClient
ProjectClient v2beta_project.ProjectServiceClient
SessionClient session.SessionServiceClient
UserClient user.UserServiceClient
AdminClient admin.AdminServiceClient
MgmtClient mgmt.ManagementServiceClient
UserClient user_v2.UserServiceClient
AuthorizationClient authorization.AuthorizationServiceClient
)
@@ -71,9 +73,10 @@ func TestMain(m *testing.M) {
SystemClient = integration.SystemClient()
OrgClient = Instance.Client.OrgV2beta
ProjectClient = Instance.Client.Projectv2Beta
SessionClient = Instance.Client.SessionV2
UserClient = Instance.Client.UserV2
AdminClient = Instance.Client.Admin
MgmtClient = Instance.Client.Mgmt
UserClient = Instance.Client.UserV2
AuthorizationClient = Instance.Client.AuthorizationV2
defer func() {
@@ -0,0 +1,626 @@
//go:build integration
package events_test
import (
"bytes"
"context"
"net"
"net/http"
"net/url"
"testing"
"time"
"github.com/muhlemmer/gu"
"github.com/pquerna/otp/totp"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"google.golang.org/protobuf/types/known/durationpb"
"github.com/zitadel/zitadel/backend/v3/domain"
"github.com/zitadel/zitadel/backend/v3/storage/database"
"github.com/zitadel/zitadel/backend/v3/storage/database/repository"
"github.com/zitadel/zitadel/internal/crypto"
zdomain "github.com/zitadel/zitadel/internal/domain"
"github.com/zitadel/zitadel/internal/integration"
"github.com/zitadel/zitadel/internal/integration/sink"
"github.com/zitadel/zitadel/pkg/grpc/session/v2"
"github.com/zitadel/zitadel/pkg/grpc/user/v2"
)
func TestServer_SessionReduces(t *testing.T) {
instanceID := Instance.ID()
sessionRepo := repository.SessionRepository()
email := integration.Email()
testUser := Instance.CreateUserTypeHuman(CTX, email)
_, err := UserClient.VerifyEmail(CTX, &user.VerifyEmailRequest{
UserId: testUser.GetId(),
VerificationCode: testUser.GetEmailCode(),
})
require.NoError(t, err)
_, err = UserClient.SetPassword(CTX, &user.SetPasswordRequest{
UserId: testUser.GetId(),
NewPassword: &user.Password{
Password: integration.UserPassword,
},
Verification: nil,
})
require.NoError(t, err)
totpSecret := registerTOTP(CTX, t, testUser.GetId())
Instance.RegisterUserPasskey(CTX, testUser.GetId())
registerOTPEmail(CTX, t, testUser.GetId())
_, err = UserClient.SetPhone(CTX, &user.SetPhoneRequest{
UserId: testUser.GetId(),
Phone: integration.Phone(),
Verification: &user.SetPhoneRequest_IsVerified{
IsVerified: true,
},
})
require.NoError(t, err)
registerOTPSMS(CTX, t, testUser.GetId())
idpID := Instance.AddGenericOAuthProvider(CTX, integration.IDPName()).GetId()
idpUserID := integration.ID()
_, err = UserClient.AddIDPLink(CTX, &user.AddIDPLinkRequest{
UserId: testUser.GetId(),
IdpLink: &user.IDPLink{
IdpId: idpID,
UserId: idpUserID,
UserName: integration.Username(),
},
})
require.NoError(t, err)
recoveryCodes := createRecoveryCodes(CTX, t, testUser.GetId())
lifetime := time.Hour
creatorID := "tester"
userAgent := &domain.SessionUserAgent{
FingerprintID: gu.Ptr(integration.ID()),
Description: gu.Ptr("description"),
IP: net.IPv4(127, 0, 0, 1),
Header: http.Header{
"User-Agent": []string{"ZITADEL-Integration-Test"},
},
}
createdSession, err := SessionClient.CreateSession(CTX, &session.CreateSessionRequest{
Checks: nil,
Metadata: nil,
Challenges: nil,
UserAgent: &session.UserAgent{
FingerprintId: userAgent.FingerprintID,
Ip: gu.Ptr(userAgent.IP.String()),
Description: userAgent.Description,
Header: map[string]*session.UserAgent_HeaderValues{
"User-Agent": {Values: []string{"ZITADEL-Integration-Test"}},
},
},
Lifetime: durationpb.New(lifetime),
})
require.NoError(t, err)
retryDuration, tick := integration.WaitForAndTickWithMaxDuration(CTX, 10*time.Second)
var updatedSession *session.SetSessionResponse
t.Run("create session reduces", func(t *testing.T) {
require.EventuallyWithT(t, func(collect *assert.CollectT) {
dbSession, err := sessionRepo.Get(CTX, pool, database.WithCondition(
sessionRepo.PrimaryKeyCondition(instanceID, createdSession.GetSessionId()),
))
require.NoError(collect, err)
assertSessionsEqual(collect, &domain.Session{
InstanceID: instanceID,
ID: createdSession.GetSessionId(),
TokenID: createdSession.GetSessionToken(),
Lifetime: lifetime,
Expiration: createdSession.GetDetails().GetChangeDate().AsTime().Add(lifetime),
UserID: "",
CreatorID: creatorID,
CreatedAt: createdSession.GetDetails().GetChangeDate().AsTime(),
UpdatedAt: createdSession.GetDetails().GetChangeDate().AsTime(),
Factors: nil,
Challenges: nil,
Metadata: nil,
UserAgent: userAgent,
}, dbSession)
}, retryDuration, tick, "session not found within %v: %v", retryDuration, err)
})
metadata := []*domain.SessionMetadata{
{
Metadata: domain.Metadata{
InstanceID: instanceID,
Key: "key1",
Value: []byte("value1"),
},
SessionID: createdSession.GetSessionId(),
},
}
t.Run("metadata set reduces", func(t *testing.T) {
updatedSession, err = SessionClient.SetSession(CTX, &session.SetSessionRequest{
SessionId: createdSession.GetSessionId(),
Checks: nil,
Metadata: map[string][]byte{"key1": []byte("value1")},
})
require.NoError(t, err)
require.EventuallyWithT(t, func(collect *assert.CollectT) {
dbSession, err := sessionRepo.Get(CTX, pool, database.WithCondition(
sessionRepo.PrimaryKeyCondition(instanceID, createdSession.GetSessionId()),
))
require.NoError(collect, err)
assertSessionsEqual(collect, &domain.Session{
InstanceID: instanceID,
ID: createdSession.GetSessionId(),
TokenID: createdSession.GetSessionToken(),
Lifetime: lifetime,
Expiration: createdSession.GetDetails().GetChangeDate().AsTime().Add(lifetime),
UserID: "",
CreatorID: creatorID,
CreatedAt: createdSession.GetDetails().GetChangeDate().AsTime(),
UpdatedAt: updatedSession.GetDetails().GetChangeDate().AsTime(),
Factors: nil,
Challenges: nil,
Metadata: metadata,
UserAgent: userAgent,
}, dbSession)
}, retryDuration, tick, "session not found within %v: %v", retryDuration, err)
})
var lifetimeUpdated time.Time
t.Run("lifetime set reduces", func(t *testing.T) {
updatedSession, err = SessionClient.SetSession(CTX, &session.SetSessionRequest{
SessionId: createdSession.GetSessionId(),
Lifetime: durationpb.New(lifetime),
})
require.NoError(t, err)
lifetimeUpdated = updatedSession.GetDetails().GetChangeDate().AsTime()
require.EventuallyWithT(t, func(collect *assert.CollectT) {
dbSession, err := sessionRepo.Get(CTX, pool, database.WithCondition(
sessionRepo.PrimaryKeyCondition(instanceID, createdSession.GetSessionId()),
))
require.NoError(collect, err)
assertSessionsEqual(collect, &domain.Session{
InstanceID: instanceID,
ID: createdSession.GetSessionId(),
TokenID: createdSession.GetSessionToken(),
Lifetime: lifetime,
Expiration: lifetimeUpdated.Add(lifetime),
UserID: "",
CreatorID: creatorID,
CreatedAt: createdSession.GetDetails().GetChangeDate().AsTime(),
UpdatedAt: updatedSession.GetDetails().GetChangeDate().AsTime(),
Factors: nil,
Challenges: nil,
Metadata: metadata,
UserAgent: userAgent,
}, dbSession)
}, retryDuration, tick, "session not found within %v: %v", retryDuration, err)
})
var firstFactorCheckTime time.Time
t.Run("checks set reduces", func(t *testing.T) {
_, err := url.Parse(Instance.CreateIntent(CTX, idpID).GetAuthUrl())
require.NoError(t, err)
expiry := time.Now().Add(1 * time.Hour)
intentID, intentToken, _, _, err := sink.SuccessfulOAuthIntent(Instance.ID(), idpID, idpUserID, testUser.GetId(), expiry)
require.NoError(t, err)
updatedSession, err = SessionClient.SetSession(CTX, &session.SetSessionRequest{
SessionId: createdSession.GetSessionId(),
Checks: &session.Checks{
User: &session.CheckUser{
Search: &session.CheckUser_LoginName{
LoginName: email,
},
},
Password: &session.CheckPassword{
Password: integration.UserPassword,
},
IdpIntent: &session.CheckIDPIntent{
IdpIntentId: intentID,
IdpIntentToken: intentToken,
},
Totp: &session.CheckTOTP{
Code: func() string {
code, err := totp.GenerateCode(totpSecret, time.Now())
require.NoError(t, err)
return code
}(),
},
RecoveryCode: &session.CheckRecoveryCode{
Code: recoveryCodes[0],
},
},
Challenges: &session.RequestChallenges{
WebAuthN: &session.RequestChallenges_WebAuthN{
Domain: Instance.Domain,
UserVerificationRequirement: session.UserVerificationRequirement_USER_VERIFICATION_REQUIREMENT_REQUIRED,
},
OtpEmail: &session.RequestChallenges_OTPEmail{
DeliveryType: &session.RequestChallenges_OTPEmail_ReturnCode_{
ReturnCode: &session.RequestChallenges_OTPEmail_ReturnCode{},
},
},
OtpSms: &session.RequestChallenges_OTPSMS{
ReturnCode: true,
},
},
})
require.NoError(t, err)
firstFactorCheckTime = updatedSession.GetDetails().GetChangeDate().AsTime()
require.EventuallyWithT(t, func(collect *assert.CollectT) {
dbSession, err := sessionRepo.Get(CTX, pool, database.WithCondition(
sessionRepo.PrimaryKeyCondition(instanceID, createdSession.GetSessionId()),
))
require.NoError(collect, err)
assertSessionsEqual(collect, &domain.Session{
InstanceID: instanceID,
ID: createdSession.GetSessionId(),
TokenID: createdSession.GetSessionToken(),
Lifetime: lifetime,
Expiration: lifetimeUpdated.Add(lifetime),
UserID: testUser.GetId(),
CreatorID: creatorID,
CreatedAt: createdSession.GetDetails().GetChangeDate().AsTime(),
UpdatedAt: updatedSession.GetDetails().GetChangeDate().AsTime(),
Factors: domain.SessionFactors{
&domain.SessionFactorUser{
UserID: testUser.GetId(),
LastVerifiedAt: firstFactorCheckTime,
},
&domain.SessionFactorPassword{
LastVerifiedAt: firstFactorCheckTime,
},
&domain.SessionFactorIdentityProviderIntent{
LastVerifiedAt: firstFactorCheckTime,
},
&domain.SessionFactorTOTP{
LastVerifiedAt: firstFactorCheckTime,
},
&domain.SessionFactorRecoveryCode{
LastVerifiedAt: firstFactorCheckTime,
},
},
Challenges: domain.SessionChallenges{
&domain.SessionChallengePasskey{
LastChallengedAt: updatedSession.GetDetails().GetChangeDate().AsTime(),
Challenge: "challenge", // placeholder
AllowedCredentialIDs: [][]byte{Instance.WebAuthN.KeyID()},
UserVerification: zdomain.UserVerificationRequirementRequired,
RPID: Instance.Domain,
},
&domain.SessionChallengeOTPEmail{
LastChallengedAt: updatedSession.GetDetails().GetChangeDate().AsTime(),
Code: &crypto.CryptoValue{
CryptoType: crypto.TypeEncryption,
Algorithm: "",
KeyID: "",
Crypted: nil,
},
Expiry: 0,
CodeReturned: true,
URLTemplate: "",
TriggeredAtOrigin: "",
},
&domain.SessionChallengeOTPSMS{
LastChallengedAt: updatedSession.GetDetails().GetChangeDate().AsTime(),
Code: &crypto.CryptoValue{
CryptoType: crypto.TypeEncryption,
Algorithm: "",
KeyID: "",
Crypted: nil,
},
Expiry: 0,
CodeReturned: true,
GeneratorID: "",
TriggeredAtOrigin: "",
},
},
Metadata: metadata,
UserAgent: userAgent,
}, dbSession)
}, retryDuration, tick, "session not found within %v: %v", retryDuration, err)
})
t.Run("checks with challenges set reduces", func(t *testing.T) {
assertionData, err := Instance.WebAuthN.CreateAssertionResponse(updatedSession.GetChallenges().GetWebAuthN().GetPublicKeyCredentialRequestOptions(), true)
require.NoError(t, err)
updatedSession, err = SessionClient.SetSession(CTX, &session.SetSessionRequest{
SessionId: createdSession.GetSessionId(),
Checks: &session.Checks{
WebAuthN: &session.CheckWebAuthN{
CredentialAssertionData: assertionData,
},
OtpEmail: &session.CheckOTP{
Code: updatedSession.GetChallenges().GetOtpEmail(),
},
OtpSms: &session.CheckOTP{
Code: updatedSession.GetChallenges().GetOtpSms(),
},
},
})
require.NoError(t, err)
require.EventuallyWithT(t, func(collect *assert.CollectT) {
dbSession, err := sessionRepo.Get(CTX, pool, database.WithCondition(
sessionRepo.PrimaryKeyCondition(instanceID, createdSession.GetSessionId()),
))
require.NoError(collect, err)
assertSessionsEqual(collect, &domain.Session{
InstanceID: instanceID,
ID: createdSession.GetSessionId(),
TokenID: createdSession.GetSessionToken(),
Lifetime: lifetime,
Expiration: lifetimeUpdated.Add(lifetime),
UserID: testUser.GetId(),
CreatorID: creatorID,
CreatedAt: createdSession.GetDetails().GetChangeDate().AsTime(),
UpdatedAt: updatedSession.GetDetails().GetChangeDate().AsTime(),
Factors: domain.SessionFactors{
&domain.SessionFactorUser{
UserID: testUser.GetId(),
LastVerifiedAt: firstFactorCheckTime,
},
&domain.SessionFactorPassword{
LastVerifiedAt: firstFactorCheckTime,
},
&domain.SessionFactorIdentityProviderIntent{
LastVerifiedAt: firstFactorCheckTime,
},
&domain.SessionFactorPasskey{
LastVerifiedAt: updatedSession.GetDetails().GetChangeDate().AsTime(),
UserVerified: true,
},
&domain.SessionFactorTOTP{
LastVerifiedAt: firstFactorCheckTime,
},
&domain.SessionFactorOTPEmail{
LastVerifiedAt: updatedSession.GetDetails().GetChangeDate().AsTime(),
},
&domain.SessionFactorOTPSMS{
LastVerifiedAt: updatedSession.GetDetails().GetChangeDate().AsTime(),
},
&domain.SessionFactorRecoveryCode{
LastVerifiedAt: firstFactorCheckTime,
},
},
Challenges: domain.SessionChallenges{},
Metadata: metadata,
UserAgent: userAgent,
}, dbSession)
}, retryDuration, tick, "session not found within %v: %v", retryDuration, err)
})
t.Run("password changed reduces", func(t *testing.T) {
passwordSet, err := UserClient.SetPassword(CTX, &user.SetPasswordRequest{
UserId: testUser.GetId(),
NewPassword: &user.Password{
Password: integration.UserPassword + integration.RandString(5),
},
Verification: &user.SetPasswordRequest_CurrentPassword{
CurrentPassword: integration.UserPassword,
},
})
require.NoError(t, err)
require.EventuallyWithT(t, func(collect *assert.CollectT) {
dbSession, err := sessionRepo.Get(CTX, pool, database.WithCondition(
sessionRepo.PrimaryKeyCondition(instanceID, createdSession.GetSessionId()),
))
require.NoError(collect, err)
assertSessionsEqual(collect, &domain.Session{
InstanceID: instanceID,
ID: createdSession.GetSessionId(),
TokenID: createdSession.GetSessionToken(),
Lifetime: lifetime,
Expiration: lifetimeUpdated.Add(lifetime),
UserID: testUser.GetId(),
CreatorID: creatorID,
CreatedAt: createdSession.GetDetails().GetChangeDate().AsTime(),
UpdatedAt: passwordSet.GetDetails().GetChangeDate().AsTime(),
Factors: domain.SessionFactors{
&domain.SessionFactorUser{
UserID: testUser.GetId(),
LastVerifiedAt: firstFactorCheckTime,
},
// password factor removed
&domain.SessionFactorIdentityProviderIntent{
LastVerifiedAt: firstFactorCheckTime,
},
&domain.SessionFactorPasskey{
LastVerifiedAt: updatedSession.GetDetails().GetChangeDate().AsTime(),
UserVerified: true,
},
&domain.SessionFactorTOTP{
LastVerifiedAt: firstFactorCheckTime,
},
&domain.SessionFactorOTPEmail{
LastVerifiedAt: updatedSession.GetDetails().GetChangeDate().AsTime(),
},
&domain.SessionFactorOTPSMS{
LastVerifiedAt: updatedSession.GetDetails().GetChangeDate().AsTime(),
},
&domain.SessionFactorRecoveryCode{
LastVerifiedAt: firstFactorCheckTime,
},
},
Challenges: domain.SessionChallenges{},
Metadata: metadata,
UserAgent: userAgent,
}, dbSession)
}, retryDuration, tick, "session not found within %v: %v", retryDuration, err)
})
t.Run("terminated reduces", func(t *testing.T) {
_, err := SessionClient.DeleteSession(CTX, &session.DeleteSessionRequest{
SessionId: createdSession.GetSessionId(),
SessionToken: gu.Ptr(updatedSession.GetSessionToken()),
})
require.NoError(t, err)
require.EventuallyWithT(t, func(collect *assert.CollectT) {
dbSession, err := sessionRepo.Get(CTX, pool, database.WithCondition(
sessionRepo.PrimaryKeyCondition(instanceID, createdSession.GetSessionId()),
))
require.ErrorIs(collect, err, new(database.NoRowFoundError))
assert.Nil(collect, dbSession)
}, retryDuration, tick, "session not found within %v: %v", retryDuration, err)
})
}
func assertSessionsEqual(t *assert.CollectT, expected, actual *domain.Session) {
t.Helper()
assert.Equal(t, expected.InstanceID, actual.InstanceID)
assert.Equal(t, expected.ID, actual.ID)
assert.NotNil(t, actual.TokenID)
assert.Equal(t, expected.Lifetime, actual.Lifetime)
assert.Equal(t, expected.Expiration.UTC(), actual.Expiration.UTC())
assert.Equal(t, expected.UserID, actual.UserID)
assert.Equal(t, expected.CreatorID, actual.CreatorID)
assert.NotNil(t, actual.CreatedAt)
assert.NotNil(t, actual.UpdatedAt)
assertSessionFactorsEqual(t, expected.Factors, actual.Factors)
assertSessionChallengesEqual(t, expected.Challenges, actual.Challenges)
assertSessionMetadataEqual(t, expected.Metadata, actual.Metadata)
assert.Equal(t, expected.UserAgent, actual.UserAgent)
}
func assertSessionMetadataEqual(t *assert.CollectT, expected, actual []*domain.SessionMetadata) {
t.Helper()
assert.Len(t, actual, len(expected))
for _, exp := range expected {
found := false
for _, act := range actual {
if exp.InstanceID == act.InstanceID &&
exp.SessionID == act.SessionID &&
exp.Key == act.Key &&
bytes.Equal(exp.Value, act.Value) {
found = true
break
}
}
assert.Truef(t, found, "expected metadata not found: %+v", exp)
}
}
func assertSessionFactorsEqual(t *assert.CollectT, expected, actual domain.SessionFactors) {
t.Helper()
assert.Len(t, actual, len(expected))
for _, exp := range expected {
found := false
for _, act := range actual {
if exp.SessionFactorType() == act.SessionFactorType() {
switch expTyped := exp.(type) {
case *domain.SessionFactorUser:
actTyped := act.(*domain.SessionFactorUser)
assert.Equal(t, expTyped.UserID, actTyped.UserID)
assert.Equal(t, expTyped.LastVerifiedAt.UTC(), actTyped.LastVerifiedAt.UTC())
case *domain.SessionFactorPassword:
actTyped := act.(*domain.SessionFactorPassword)
assert.Equal(t, expTyped.LastVerifiedAt.UTC(), actTyped.LastVerifiedAt.UTC())
case *domain.SessionFactorIdentityProviderIntent:
actTyped := act.(*domain.SessionFactorIdentityProviderIntent)
assert.Equal(t, expTyped.LastVerifiedAt.UTC(), actTyped.LastVerifiedAt.UTC())
case *domain.SessionFactorPasskey:
actTyped := act.(*domain.SessionFactorPasskey)
assert.Equal(t, expTyped.LastVerifiedAt.UTC(), actTyped.LastVerifiedAt.UTC())
assert.Equal(t, expTyped.UserVerified, actTyped.UserVerified)
case *domain.SessionFactorTOTP:
actTyped := act.(*domain.SessionFactorTOTP)
assert.Equal(t, expTyped.LastVerifiedAt.UTC(), actTyped.LastVerifiedAt.UTC())
case *domain.SessionFactorOTPEmail:
actTyped := act.(*domain.SessionFactorOTPEmail)
assert.Equal(t, expTyped.LastVerifiedAt.UTC(), actTyped.LastVerifiedAt.UTC())
case *domain.SessionFactorOTPSMS:
actTyped := act.(*domain.SessionFactorOTPSMS)
assert.Equal(t, expTyped.LastVerifiedAt.UTC(), actTyped.LastVerifiedAt.UTC())
case *domain.SessionFactorRecoveryCode:
actTyped := act.(*domain.SessionFactorRecoveryCode)
assert.Equal(t, expTyped.LastVerifiedAt.UTC(), actTyped.LastVerifiedAt.UTC())
}
found = true
break
}
}
assert.Truef(t, found, "expected factor not found: %+v", exp)
}
}
func assertSessionChallengesEqual(t *assert.CollectT, expected, actual domain.SessionChallenges) {
t.Helper()
assert.Len(t, actual, len(expected))
for _, exp := range expected {
found := false
for _, act := range actual {
if exp.SessionChallengeType() == act.SessionChallengeType() {
switch expTyped := exp.(type) {
case *domain.SessionChallengePasskey:
actTyped := act.(*domain.SessionChallengePasskey)
assert.Equal(t, expTyped.LastChallengedAt.UTC(), actTyped.LastChallengedAt.UTC())
assert.NotEmpty(t, actTyped.Challenge)
assert.Equal(t, expTyped.AllowedCredentialIDs, actTyped.AllowedCredentialIDs)
assert.Equal(t, expTyped.RPID, actTyped.RPID)
assert.Equal(t, expTyped.UserVerification, actTyped.UserVerification)
case *domain.SessionChallengeOTPEmail:
actTyped := act.(*domain.SessionChallengeOTPEmail)
assert.Equal(t, expTyped.LastChallengedAt.UTC(), actTyped.LastChallengedAt.UTC())
assert.Equal(t, expTyped.Code.CryptoType, actTyped.Code.CryptoType)
case *domain.SessionChallengeOTPSMS:
actTyped := act.(*domain.SessionChallengeOTPSMS)
assert.Equal(t, expTyped.LastChallengedAt.UTC(), actTyped.LastChallengedAt.UTC())
assert.Equal(t, expTyped.Code.CryptoType, actTyped.Code.CryptoType)
}
found = true
break
}
}
assert.Truef(t, found, "expected challenge not found: %+v", exp)
}
}
func registerTOTP(ctx context.Context, t *testing.T, userID string) (secret string) {
resp, err := UserClient.RegisterTOTP(ctx, &user.RegisterTOTPRequest{
UserId: userID,
})
require.NoError(t, err)
secret = resp.GetSecret()
code, err := totp.GenerateCode(secret, time.Now())
require.NoError(t, err)
_, err = UserClient.VerifyTOTPRegistration(ctx, &user.VerifyTOTPRegistrationRequest{
UserId: userID,
Code: code,
})
require.NoError(t, err)
return secret
}
func registerOTPSMS(ctx context.Context, t *testing.T, userID string) {
_, err := UserClient.AddOTPSMS(ctx, &user.AddOTPSMSRequest{
UserId: userID,
})
require.NoError(t, err)
}
func registerOTPEmail(ctx context.Context, t *testing.T, userID string) {
_, err := UserClient.AddOTPEmail(ctx, &user.AddOTPEmailRequest{
UserId: userID,
})
require.NoError(t, err)
}
func createRecoveryCodes(ctx context.Context, t *testing.T, userID string) (secret []string) {
resp, err := UserClient.GenerateRecoveryCodes(ctx, &user.GenerateRecoveryCodesRequest{
UserId: userID,
Count: 5,
})
require.NoError(t, err)
return resp.GetRecoveryCodes()
}
+4 -4
View File
@@ -60,9 +60,9 @@ const (
// TextOperationContainsIgnoreCase checks if the first string contains the second ignoring case
TextOperationContainsIgnoreCase // LIKE
// TextOperationEndsWith checks if the first string ends with the second ignoring case
// TextOperationEndsWithIgnoreCase checks if the first string ends with the second ignoring case
TextOperationEndsWithIgnoreCase // LIKE
// TextOperationStartsWith checks if the first string starts with the second ignoring case
// TextOperationStartsWithIgnoreCase checks if the first string starts with the second ignoring case
TextOperationStartsWithIgnoreCase // LIKE
// TextOperationEqualIgnoreCase compares two strings for equality ignoring case
TextOperationEqualIgnoreCase // =
@@ -133,11 +133,11 @@ const (
// NumberOperationLessThan compares two numbers to check if the first is less than the second.
NumberOperationLessThan // <
// NumberOperationLessThanOrEqual compares two numbers to check if the first is less than or equal to the second.
NumberOperationAtLeast // <=
NumberOperationLessThanOrEqual // <=
// NumberOperationGreaterThan compares two numbers to check if the first is greater than the second.
NumberOperationGreaterThan // >
// NumberOperationGreaterThanOrEqual compares two numbers to check if the first is greater than or equal to the second.
NumberOperationAtMost // >=
NumberOperationGreaterThanOrEqual // >=
)
func writeNumberOperation[T Number](builder *StatementBuilder, col Column, op NumberOperation, value any) {
@@ -28,12 +28,12 @@ func _NumberOperationNoOp() {
_ = x[NumberOperationEqual-(1)]
_ = x[NumberOperationNotEqual-(2)]
_ = x[NumberOperationLessThan-(3)]
_ = x[NumberOperationAtLeast-(4)]
_ = x[NumberOperationLessThanOrEqual-(4)]
_ = x[NumberOperationGreaterThan-(5)]
_ = x[NumberOperationAtMost-(6)]
_ = x[NumberOperationGreaterThanOrEqual-(6)]
}
var _NumberOperationValues = []NumberOperation{NumberOperationEqual, NumberOperationNotEqual, NumberOperationLessThan, NumberOperationAtLeast, NumberOperationGreaterThan, NumberOperationAtMost}
var _NumberOperationValues = []NumberOperation{NumberOperationEqual, NumberOperationNotEqual, NumberOperationLessThan, NumberOperationLessThanOrEqual, NumberOperationGreaterThan, NumberOperationGreaterThanOrEqual}
var _NumberOperationNameToValueMap = map[string]NumberOperation{
_NumberOperationName[0:1]: NumberOperationEqual,
@@ -42,12 +42,12 @@ var _NumberOperationNameToValueMap = map[string]NumberOperation{
_NumberOperationLowerName[1:3]: NumberOperationNotEqual,
_NumberOperationName[3:4]: NumberOperationLessThan,
_NumberOperationLowerName[3:4]: NumberOperationLessThan,
_NumberOperationName[4:6]: NumberOperationAtLeast,
_NumberOperationLowerName[4:6]: NumberOperationAtLeast,
_NumberOperationName[4:6]: NumberOperationLessThanOrEqual,
_NumberOperationLowerName[4:6]: NumberOperationLessThanOrEqual,
_NumberOperationName[6:7]: NumberOperationGreaterThan,
_NumberOperationLowerName[6:7]: NumberOperationGreaterThan,
_NumberOperationName[7:9]: NumberOperationAtMost,
_NumberOperationLowerName[7:9]: NumberOperationAtMost,
_NumberOperationName[7:9]: NumberOperationGreaterThanOrEqual,
_NumberOperationLowerName[7:9]: NumberOperationGreaterThanOrEqual,
}
var _NumberOperationNames = []string{
@@ -181,7 +181,7 @@ func Test_writeOperation(t *testing.T) {
{
name: "number less than or equal",
write: func(builder *StatementBuilder) {
writeNumberOperation[int](builder, NewColumn("table", "column"), NumberOperationAtLeast, 123)
writeNumberOperation[int](builder, NewColumn("table", "column"), NumberOperationLessThanOrEqual, 123)
},
want: want{
stmt: "table.column <= $1",
@@ -201,7 +201,7 @@ func Test_writeOperation(t *testing.T) {
{
name: "number greater than or equal",
write: func(builder *StatementBuilder) {
writeNumberOperation[int](builder, NewColumn("table", "column"), NumberOperationAtMost, 123)
writeNumberOperation[int](builder, NewColumn("table", "column"), NumberOperationGreaterThanOrEqual, 123)
},
want: want{
stmt: "table.column >= $1",
@@ -0,0 +1,48 @@
package repository
import (
"encoding/json"
"github.com/zitadel/zitadel/backend/v3/storage/database"
)
type JSON[T any] struct {
Value T
}
func (j *JSON[T]) Scan(src any) (err error) {
var rawJSON []byte
switch s := src.(type) {
case string:
rawJSON = []byte(s)
case []byte:
rawJSON = s
case nil:
return nil
default:
return ErrScanSource
}
if err = j.UnmarshalJSON(rawJSON); err != nil {
return database.NewScanError(err)
}
return nil
}
// UnmarshalJSON allows encoding/json to decode nested JSON into JSON[T] fields.
// This makes [json.Unmarshal] work for fields inside aggregated JSON (e.g. JSON arrays).
func (j *JSON[T]) UnmarshalJSON(data []byte) error {
if len(data) == 0 || string(data) == "null" {
return nil
}
var v T
if err := json.Unmarshal(data, &v); err != nil {
return err
}
j.Value = v
return nil
}
// MarshalJSON encodes the wrapped value or null if nil.
func (j JSON[T]) MarshalJSON() ([]byte, error) {
return json.Marshal(j.Value)
}
@@ -0,0 +1,102 @@
package repository_test
import (
"testing"
"github.com/stretchr/testify/require"
"github.com/zitadel/zitadel/backend/v3/storage/database"
"github.com/zitadel/zitadel/backend/v3/storage/database/repository"
)
func TestJSON_Scan(t *testing.T) {
type want struct {
err error
res *testObject
}
tests := []struct {
name string
src any
want want
}{
{
name: "nil",
src: nil,
want: want{
err: nil,
res: nil,
},
},
{
name: "[]byte with data",
src: []byte(`{"id":"1","number":1,"active":true}`),
want: want{
err: nil,
res: &testObject{ID: "1", Number: 1, Active: true},
},
},
{
name: "[]byte without data",
src: []byte(`{}`),
want: want{
err: nil,
res: &testObject{},
},
},
{
name: "nil []byte",
src: []byte(nil),
want: want{
err: nil,
res: nil,
},
},
{
name: "string with data",
src: `{"id":"2","number":2,"active":false}`,
want: want{
err: nil,
res: &testObject{ID: "2", Number: 2, Active: false},
},
},
{
name: "string without data",
src: `{}`,
want: want{
err: nil,
res: &testObject{},
},
},
{
name: "empty string",
src: ``,
want: want{
err: nil,
res: nil,
},
},
{
name: "unsupported type",
src: 12345,
want: want{
err: repository.ErrScanSource,
res: nil,
},
},
{
name: "invalid json",
src: []byte(`this is not json`),
want: want{
err: new(database.ScanError),
res: nil,
},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
var a repository.JSON[testObject]
gotErr := a.Scan(tt.src)
require.ErrorIs(t, gotErr, tt.want.err)
})
}
}
@@ -0,0 +1,655 @@
package repository
import (
"context"
"fmt"
"time"
"github.com/muhlemmer/gu"
"github.com/zitadel/zitadel/backend/v3/domain"
"github.com/zitadel/zitadel/backend/v3/storage/database"
)
// -------------------------------------------------------------
// repository
// -------------------------------------------------------------
var _ domain.SessionRepository = (*session)(nil)
type session struct {
factorRepo sessionFactor
metadataRepo sessionMetadata
userAgentRepo sessionUserAgent
}
func (s session) qualifiedTableName() string {
return "zitadel.sessions"
}
func (s session) unqualifiedTableName() string {
return "sessions"
}
func SessionRepository() domain.SessionRepository {
return new(session)
}
const querySessionStmt = `
SELECT
sessions.instance_id
, sessions.id
, sessions.token_id
, sessions.lifetime
, sessions.expiration
, sessions.user_id
, sessions.creator_id
, sessions.created_at
, sessions.updated_at
, jsonb_agg(distinct
jsonb_build_object(
'instanceId', session_factors.instance_id
, 'sessionId', session_factors.session_id
, 'type', session_factors.type
, 'lastChallengedAt', session_factors.last_challenged_at
, 'challengedPayload', session_factors.challenged_payload
, 'lastVerifiedAt', session_factors.last_verified_at
, 'verifiedPayload', session_factors.verified_payload
)
)
FILTER (WHERE session_factors.session_id IS NOT NULL) AS factors
, jsonb_agg(distinct
jsonb_build_object(
'instanceId', session_metadata.instance_id
, 'sessionId', session_metadata.session_id
, 'key', session_metadata.key
, 'value', encode(session_metadata.value, 'base64')
, 'createdAt', session_metadata.created_at
, 'updatedAt', session_metadata.updated_at
)
)
FILTER (WHERE session_metadata.session_id IS NOT NULL) AS metadata
, CASE WHEN session_user_agents.fingerprint_id IS NOT NULL THEN
jsonb_build_object(
'fingerprintId', session_user_agents.fingerprint_id
, 'description', session_user_agents.description
, 'ip', session_user_agents.ip
, 'headers', session_user_agents.headers
)
END as user_agent
FROM zitadel.sessions`
// Get implements [domain.SessionRepository].
func (s session) Get(ctx context.Context, client database.QueryExecutor, opts ...database.QueryOption) (*domain.Session, error) {
opts = append(opts,
s.joinFactors(),
s.joinMetadata(),
s.joinUserAgent(),
database.WithGroupBy(
s.InstanceIDColumn(),
s.IDColumn(),
s.userAgentRepo.fingerprintIDColumn(),
s.userAgentRepo.descriptionColumn(),
s.userAgentRepo.ipColumn(),
s.userAgentRepo.headersColumn(),
),
)
options := new(database.QueryOpts)
for _, opt := range opts {
opt(options)
}
if !options.Condition.IsRestrictingColumn(s.InstanceIDColumn()) {
return nil, database.NewMissingConditionError(s.InstanceIDColumn())
}
var builder database.StatementBuilder
builder.WriteString(querySessionStmt)
options.Write(&builder)
return scanSession(ctx, client, &builder)
}
// List implements [domain.SessionRepository].
func (s session) List(ctx context.Context, client database.QueryExecutor, opts ...database.QueryOption) ([]*domain.Session, error) {
opts = append(opts,
s.joinFactors(),
s.joinMetadata(),
s.joinUserAgent(),
database.WithGroupBy(
s.InstanceIDColumn(),
s.IDColumn(),
s.userAgentRepo.fingerprintIDColumn(),
s.userAgentRepo.descriptionColumn(),
s.userAgentRepo.ipColumn(),
s.userAgentRepo.headersColumn(),
),
)
options := new(database.QueryOpts)
for _, opt := range opts {
opt(options)
}
if options.Condition == nil || !options.Condition.IsRestrictingColumn(s.InstanceIDColumn()) {
return nil, database.NewMissingConditionError(s.InstanceIDColumn())
}
var builder database.StatementBuilder
builder.WriteString(querySessionStmt)
options.Write(&builder)
return scanSessions(ctx, client, &builder)
}
const upsertSessionUserAgentStmt = `
WITH user_agent AS (
INSERT INTO zitadel.session_user_agents(
instance_id, fingerprint_id, description, ip, headers
)
VALUES ($1, $2, $3, $4, $5)
ON CONFLICT (instance_id, fingerprint_id)
DO UPDATE SET description = EXCLUDED.description, ip = EXCLUDED.ip, headers = excluded.headers
) `
// Create implements [domain.SessionRepository].
func (s session) Create(ctx context.Context, client database.QueryExecutor, session *domain.Session) error {
var createdAt any = database.DefaultInstruction
if !session.CreatedAt.IsZero() {
createdAt = session.CreatedAt
}
var updatedAt = createdAt
if !session.UpdatedAt.IsZero() {
updatedAt = session.UpdatedAt
}
builder := new(database.StatementBuilder)
var fingerprintID *string
if session.UserAgent != nil {
builder = database.NewStatementBuilder(upsertSessionUserAgentStmt,
session.InstanceID,
session.UserAgent.FingerprintID,
session.UserAgent.Description,
session.UserAgent.IP,
session.UserAgent.Header,
)
fingerprintID = session.UserAgent.FingerprintID
}
builder.WriteString(`INSERT INTO ` + s.qualifiedTableName() + ` (instance_id, id, lifetime, creator_id, user_agent_id, created_at, updated_at) VALUES ( `)
builder.WriteArgs(session.InstanceID, session.ID, session.Lifetime, session.CreatorID, fingerprintID, createdAt, updatedAt)
builder.WriteString(` ) RETURNING created_at, updated_at`)
return client.QueryRow(ctx, builder.String(), builder.Args()...).Scan(&session.CreatedAt, &session.UpdatedAt)
}
// Update implements [domain.SessionRepository].
func (s session) Update(ctx context.Context, client database.QueryExecutor, condition database.Condition, changes ...database.Change) (int64, error) {
if len(changes) == 0 {
return 0, database.ErrNoChanges
}
if !condition.IsRestrictingColumn(s.InstanceIDColumn()) {
return 0, database.NewMissingConditionError(s.InstanceIDColumn())
}
if !database.Changes(changes).IsOnColumn(s.UpdatedAtColumn()) {
changes = append(changes, database.NewChange(s.UpdatedAtColumn(), database.DefaultInstruction))
}
var builder database.StatementBuilder
builder.WriteString("WITH existing_session AS (SELECT * FROM zitadel.sessions ")
writeCondition(&builder, condition)
builder.WriteString(") ")
for i, change := range changes {
sessionCTE(change, i, 0, &builder)
}
builder.WriteString("UPDATE zitadel.sessions SET ")
if err := database.Changes(changes).Write(&builder); err != nil {
return 0, err
}
writeCondition(&builder, condition)
return client.Exec(ctx, builder.String(), builder.Args()...)
}
func sessionCTE(change database.Change, i, j int, builder *database.StatementBuilder) {
if multi, ok := change.(database.Changes); ok {
for j, ch := range multi {
sessionCTE(ch, i, j, builder)
}
}
if cte, ok := change.(database.CTEChange); ok {
name := fmt.Sprintf("cte_%d_%d", i, j)
fmt.Fprintf(builder, ", %s as (", name)
cte.SetName(name)
cte.WriteCTE(builder)
builder.WriteString(") ")
}
}
// Delete implements [domain.SessionRepository].
func (s session) Delete(ctx context.Context, client database.QueryExecutor, condition database.Condition) (int64, error) {
if !condition.IsRestrictingColumn(s.InstanceIDColumn()) {
return 0, database.NewMissingConditionError(s.InstanceIDColumn())
}
var builder database.StatementBuilder
builder.WriteString("DELETE FROM zitadel.sessions")
writeCondition(&builder, condition)
return client.Exec(ctx, builder.String(), builder.Args()...)
}
// -------------------------------------------------------------
// changes
// -------------------------------------------------------------
// SetUpdatedAt implements [domain.sessionChanges].
func (s session) SetUpdatedAt(updatedAt time.Time) database.Change {
return database.NewChange(s.UpdatedAtColumn(), updatedAt)
}
// SetToken implements [domain.sessionChanges].
func (s session) SetToken(token string) database.Change {
return database.NewChange(s.TokenIDColumn(), token)
}
// SetLifetime implements [domain.sessionChanges].
func (s session) SetLifetime(lifetime time.Duration) database.Change {
return database.NewChange(s.LifetimeColumn(), lifetime)
}
// SetChallenge implements [domain.sessionChanges].
func (s session) SetChallenge(challenge domain.SessionChallenge) database.Change {
switch c := challenge.(type) {
case *domain.SessionChallengePasskey:
return database.NewCTEChange(
func(builder *database.StatementBuilder) {
builder.WriteString("INSERT INTO zitadel.session_factors (instance_id, session_id, type, last_challenged_at, challenged_payload) SELECT instance_id, id, ")
builder.WriteArgs(domain.SessionFactorTypePasskey, c.LastChallengedAt, c)
builder.WriteString(" FROM existing_session ON CONFLICT (instance_id, session_id, type) DO UPDATE SET last_challenged_at = EXCLUDED.last_challenged_at, challenged_payload = EXCLUDED.challenged_payload")
}, nil,
)
case *domain.SessionChallengeOTPSMS:
return database.NewCTEChange(
func(builder *database.StatementBuilder) {
builder.WriteString("INSERT INTO zitadel.session_factors (instance_id, session_id, type, last_challenged_at, challenged_payload) SELECT instance_id, id, ")
builder.WriteArgs(domain.SessionFactorTypeOTPSMS, c.LastChallengedAt, c)
builder.WriteString(" FROM existing_session ON CONFLICT (instance_id, session_id, type) DO UPDATE SET last_challenged_at = EXCLUDED.last_challenged_at, challenged_payload = EXCLUDED.challenged_payload")
}, nil,
)
case *domain.SessionChallengeOTPEmail:
return database.NewCTEChange(
func(builder *database.StatementBuilder) {
builder.WriteString("INSERT INTO zitadel.session_factors (instance_id, session_id, type, last_challenged_at, challenged_payload) SELECT instance_id, id, ")
builder.WriteArgs(domain.SessionFactorTypeOTPEmail, c.LastChallengedAt, c)
builder.WriteString(" FROM existing_session ON CONFLICT (instance_id, session_id, type) DO UPDATE SET last_challenged_at = EXCLUDED.last_challenged_at, challenged_payload = EXCLUDED.challenged_payload")
}, nil,
)
}
return nil
}
// SetFactor implements [domain.sessionChanges].
func (s session) SetFactor(factor domain.SessionFactor) database.Change {
switch f := factor.(type) {
case *domain.SessionFactorUser:
return database.NewChanges(
database.NewChange(s.UserIDColumn(), f.UserID),
database.NewCTEChange(
func(builder *database.StatementBuilder) {
builder.WriteString("INSERT INTO zitadel.session_factors (instance_id, session_id, type, last_verified_at, verified_payload) SELECT instance_id, id, ")
builder.WriteArgs(domain.SessionFactorTypeUser, f.LastVerifiedAt, f)
builder.WriteString(" FROM existing_session ON CONFLICT (instance_id, session_id, type) DO UPDATE SET last_verified_at = EXCLUDED.last_verified_at, verified_payload = EXCLUDED.verified_payload")
}, nil,
),
)
case *domain.SessionFactorPassword:
return database.NewCTEChange(
func(builder *database.StatementBuilder) {
builder.WriteString("INSERT INTO zitadel.session_factors (instance_id, session_id, type, last_verified_at) SELECT instance_id, id, ")
builder.WriteArgs(domain.SessionFactorTypePassword, f.LastVerifiedAt)
builder.WriteString(" FROM existing_session ON CONFLICT (instance_id, session_id, type) DO UPDATE SET last_verified_at = EXCLUDED.last_verified_at")
}, nil,
)
case *domain.SessionFactorIdentityProviderIntent:
return database.NewCTEChange(
func(builder *database.StatementBuilder) {
builder.WriteString("INSERT INTO zitadel.session_factors (instance_id, session_id, type, last_verified_at) SELECT instance_id, id, ")
builder.WriteArgs(domain.SessionFactorTypeIdentityProviderIntent, f.LastVerifiedAt)
builder.WriteString(" FROM existing_session ON CONFLICT (instance_id, session_id, type) DO UPDATE SET last_verified_at = EXCLUDED.last_verified_at")
}, nil,
)
case *domain.SessionFactorPasskey:
return database.NewCTEChange(
func(builder *database.StatementBuilder) {
builder.WriteString("INSERT INTO zitadel.session_factors (instance_id, session_id, type, last_verified_at, verified_payload) SELECT instance_id, id, ")
builder.WriteArgs(domain.SessionFactorTypePasskey, f.LastVerifiedAt, f)
builder.WriteString(" FROM existing_session ON CONFLICT (instance_id, session_id, type) DO UPDATE SET last_verified_at = EXCLUDED.last_verified_at, verified_payload = EXCLUDED.verified_payload")
}, nil,
)
case *domain.SessionFactorTOTP:
return database.NewCTEChange(
func(builder *database.StatementBuilder) {
builder.WriteString("INSERT INTO zitadel.session_factors (instance_id, session_id, type, last_verified_at) SELECT instance_id, id, ")
builder.WriteArgs(domain.SessionFactorTypeTOTP, f.LastVerifiedAt)
builder.WriteString(" FROM existing_session ON CONFLICT (instance_id, session_id, type) DO UPDATE SET last_verified_at = EXCLUDED.last_verified_at")
}, nil,
)
case *domain.SessionFactorOTPSMS:
return database.NewCTEChange(
func(builder *database.StatementBuilder) {
builder.WriteString("INSERT INTO zitadel.session_factors (instance_id, session_id, type, last_verified_at) SELECT instance_id, id, ")
builder.WriteArgs(domain.SessionFactorTypeOTPSMS, f.LastVerifiedAt)
builder.WriteString(" FROM existing_session ON CONFLICT (instance_id, session_id, type) DO UPDATE SET last_verified_at = EXCLUDED.last_verified_at")
}, nil,
)
case *domain.SessionFactorOTPEmail:
return database.NewCTEChange(
func(builder *database.StatementBuilder) {
builder.WriteString("INSERT INTO zitadel.session_factors (instance_id, session_id, type, last_verified_at) SELECT instance_id, id, ")
builder.WriteArgs(domain.SessionFactorTypeOTPEmail, f.LastVerifiedAt)
builder.WriteString(" FROM existing_session ON CONFLICT (instance_id, session_id, type) DO UPDATE SET last_verified_at = EXCLUDED.last_verified_at")
}, nil,
)
case *domain.SessionFactorRecoveryCode:
return database.NewCTEChange(
func(builder *database.StatementBuilder) {
builder.WriteString("INSERT INTO zitadel.session_factors (instance_id, session_id, type, last_verified_at) SELECT instance_id, id, ")
builder.WriteArgs(domain.SessionFactorTypeRecoveryCode, f.LastVerifiedAt)
builder.WriteString(" FROM existing_session ON CONFLICT (instance_id, session_id, type) DO UPDATE SET last_verified_at = EXCLUDED.last_verified_at")
}, nil,
)
default:
return nil
}
}
// ClearFactor implements [domain.sessionChanges].
func (s session) ClearFactor(factorType domain.SessionFactorType) database.Change {
return database.NewCTEChange(
func(builder *database.StatementBuilder) {
builder.WriteString("UPDATE zitadel.session_factors sf SET last_verified_at = NULL FROM existing_session es WHERE sf.instance_id = es.instance_id AND sf.session_id = es.id AND sf.type = ")
builder.WriteArg(factorType)
}, nil)
}
// SetMetadata implements [domain.sessionChanges].
func (s session) SetMetadata(metadata []*domain.SessionMetadata) database.Change {
changes := make([]database.Change, len(metadata)+1)
keys := make([]any, len(metadata))
for i, md := range metadata {
keys[i] = md.Key
changes[i] = database.NewCTEChange(
func(builder *database.StatementBuilder) {
builder.WriteString("INSERT INTO zitadel.session_metadata (instance_id, session_id, key, value) SELECT instance_id, id, ")
builder.WriteArgs(md.Key, md.Value)
builder.WriteString(" FROM existing_session ON CONFLICT (instance_id, session_id, key) DO UPDATE SET value = EXCLUDED.value")
}, nil,
)
}
changes[len(metadata)] = database.NewCTEChange(
func(builder *database.StatementBuilder) {
builder.WriteString("DELETE FROM zitadel.session_metadata WHERE instance_id = (SELECT instance_id FROM existing_session) AND session_id = (SELECT id from existing_session) AND key NOT IN (")
builder.WriteArgs(keys...)
builder.WriteString(")")
}, nil)
return database.NewChanges(changes...)
}
// -------------------------------------------------------------
// conditions
// -------------------------------------------------------------
// PrimaryKeyCondition implements [domain.sessionConditions].
func (s session) PrimaryKeyCondition(instanceID, sessionID string) database.Condition {
return database.And(
s.InstanceIDCondition(instanceID),
s.IDCondition(sessionID),
)
}
// InstanceIDCondition implements [domain.sessionConditions].
func (s session) InstanceIDCondition(instanceID string) database.Condition {
return database.NewTextCondition(s.InstanceIDColumn(), database.TextOperationEqual, instanceID)
}
// IDCondition implements [domain.sessionConditions].
func (s session) IDCondition(sessionID string) database.Condition {
return database.NewTextCondition(s.IDColumn(), database.TextOperationEqual, sessionID)
}
// UserAgentIDCondition implements [domain.sessionConditions].
func (s session) UserAgentIDCondition(userAgentID string) database.Condition {
return database.NewTextCondition(s.userAgentIDColumn(), database.TextOperationEqual, userAgentID)
}
// UserIDCondition implements [domain.sessionConditions].
func (s session) UserIDCondition(userID string) database.Condition {
return database.NewTextCondition(s.UserIDColumn(), database.TextOperationEqual, userID)
}
// CreatorIDCondition implements [domain.sessionConditions].
func (s session) CreatorIDCondition(creatorID string) database.Condition {
return database.NewTextCondition(s.CreatorIDColumn(), database.TextOperationEqual, creatorID)
}
// ExpirationCondition implements [domain.sessionConditions].
func (s session) ExpirationCondition(op database.NumberOperation, expiration time.Time) database.Condition {
return database.NewNumberCondition(s.ExpirationColumn(), op, expiration)
}
// CreatedAtCondition implements [domain.sessionConditions].
func (s session) CreatedAtCondition(op database.NumberOperation, createdAt time.Time) database.Condition {
return database.NewNumberCondition(s.CreatedAtColumn(), op, createdAt)
}
// UpdatedAtCondition implements [domain.sessionConditions].
func (s session) UpdatedAtCondition(op database.NumberOperation, updatedAt time.Time) database.Condition {
return database.NewNumberCondition(s.UpdatedAtColumn(), op, updatedAt)
}
// ExistsFactor implements [domain.sessionConditions].
func (s session) ExistsFactor(cond database.Condition) database.Condition {
return database.Exists(
s.factorRepo.qualifiedTableName(),
database.And(
database.NewColumnCondition(s.InstanceIDColumn(), s.factorRepo.instanceIDColumn()),
database.NewColumnCondition(s.IDColumn(), s.factorRepo.sessionIDColumn()),
cond,
),
)
}
// FactorConditions implements [domain.sessionConditions].
func (s session) FactorConditions() domain.SessionFactorConditions {
return s.factorRepo
}
// ExistsMetadata implements [domain.sessionConditions].
func (s session) ExistsMetadata(cond database.Condition) database.Condition {
return database.Exists(
s.metadataRepo.qualifiedTableName(),
database.And(
database.NewColumnCondition(s.InstanceIDColumn(), s.metadataRepo.instanceIDColumn()),
database.NewColumnCondition(s.IDColumn(), s.metadataRepo.sessionIDColumn()),
cond,
),
)
}
// MetadataConditions implements [domain.sessionConditions].
func (s session) MetadataConditions() domain.SessionMetadataConditions {
return s.metadataRepo
}
// -------------------------------------------------------------
// columns
// -------------------------------------------------------------
// PrimaryKeyColumns implements [domain.Repository].
func (s session) PrimaryKeyColumns() []database.Column {
return []database.Column{
s.InstanceIDColumn(),
s.IDColumn(),
}
}
// InstanceIDColumn implements [domain.sessionColumns].
func (s session) InstanceIDColumn() database.Column {
return database.NewColumn(s.unqualifiedTableName(), "instance_id")
}
// IDColumn implements [domain.sessionColumns].
func (s session) IDColumn() database.Column {
return database.NewColumn(s.unqualifiedTableName(), "id")
}
// TokenIDColumn implements [domain.sessionColumns].
func (s session) TokenIDColumn() database.Column {
return database.NewColumn(s.unqualifiedTableName(), "token_id")
}
// LifetimeColumn implements [domain.sessionColumns].
func (s session) LifetimeColumn() database.Column {
return database.NewColumn(s.unqualifiedTableName(), "lifetime")
}
// ExpirationColumn implements [domain.sessionColumns].
func (s session) ExpirationColumn() database.Column {
return database.NewColumn(s.unqualifiedTableName(), "expiration")
}
// UserIDColumn implements [domain.sessionColumns].
func (s session) UserIDColumn() database.Column {
return database.NewColumn(s.unqualifiedTableName(), "user_id")
}
// UserAgentIDColumn implements [domain.sessionColumns].
func (s session) userAgentIDColumn() database.Column {
return database.NewColumn(s.unqualifiedTableName(), "user_agent_id")
}
// CreatorIDColumn implements [domain.sessionColumns].
func (s session) CreatorIDColumn() database.Column {
return database.NewColumn(s.unqualifiedTableName(), "creator_id")
}
// CreatedAtColumn implements [domain.sessionColumns].
func (s session) CreatedAtColumn() database.Column {
return database.NewColumn(s.unqualifiedTableName(), "created_at")
}
// UpdatedAtColumn implements [domain.sessionColumns].
func (s session) UpdatedAtColumn() database.Column {
return database.NewColumn(s.unqualifiedTableName(), "updated_at")
}
// -------------------------------------------------------------
// scanners
// -------------------------------------------------------------
type rawSession struct {
*domain.Session
TokenID *string `json:"tokenID" db:"token_id"`
Lifetime *time.Duration `json:"lifetime" db:"lifetime"`
Expiration *time.Time `json:"expiration" db:"expiration"`
UserID *string `json:"userID" db:"user_id"`
CreatorID *string `json:"creatorID" db:"creator_id"`
Factors JSONArray[rawFactor] `json:"factors,omitempty" db:"factors"`
Metadata JSONArray[domain.SessionMetadata] `json:"metadata,omitempty" db:"metadata"`
}
func scanSession(ctx context.Context, querier database.Querier, builder *database.StatementBuilder) (*domain.Session, error) {
rows, err := querier.Query(ctx, builder.String(), builder.Args()...)
if err != nil {
return nil, err
}
raw := new(rawSession)
if err := rows.(database.CollectableRows).CollectExactlyOneRow(raw); err != nil {
return nil, err
}
return rawSessionToDomain(raw)
}
func scanSessions(ctx context.Context, querier database.Querier, builder *database.StatementBuilder) ([]*domain.Session, error) {
rows, err := querier.Query(ctx, builder.String(), builder.Args()...)
if err != nil {
return nil, err
}
var sessions []*rawSession
if err := rows.(database.CollectableRows).Collect(&sessions); err != nil {
return nil, err
}
result := make([]*domain.Session, len(sessions))
for i, session := range sessions {
result[i], err = rawSessionToDomain(session)
if err != nil {
return nil, err
}
}
return result, nil
}
func rawSessionToDomain(raw *rawSession) (*domain.Session, error) {
raw.Session.TokenID = gu.Value(raw.TokenID)
raw.Session.Lifetime = gu.Value(raw.Lifetime)
raw.Session.Expiration = gu.Value(raw.Expiration)
raw.Session.UserID = gu.Value(raw.UserID)
raw.Session.CreatorID = gu.Value(raw.CreatorID)
for _, factor := range raw.Factors {
f, ch, err := factor.ToDomain()
if err != nil {
return nil, err
}
if f != nil {
raw.Session.Factors.AppendTo(f)
}
if ch != nil {
raw.Challenges.AppendTo(ch)
}
}
raw.Session.Metadata = raw.Metadata
return raw.Session, nil
}
// -------------------------------------------------------------
// sub repositories
// -------------------------------------------------------------
func (s session) joinUserAgent() database.QueryOption {
columns := make([]database.Condition, 0, 3)
columns = append(columns,
database.NewColumnCondition(s.InstanceIDColumn(), s.userAgentRepo.instanceIDColumn()),
database.NewColumnCondition(s.userAgentIDColumn(), s.userAgentRepo.fingerprintIDColumn()),
)
return database.WithLeftJoin(
s.userAgentRepo.qualifiedTableName(),
database.And(columns...),
)
}
func (s session) joinFactors() database.QueryOption {
columns := make([]database.Condition, 0, 3)
columns = append(columns,
database.NewColumnCondition(s.InstanceIDColumn(), s.factorRepo.instanceIDColumn()),
database.NewColumnCondition(s.IDColumn(), s.factorRepo.sessionIDColumn()),
)
return database.WithLeftJoin(
s.factorRepo.qualifiedTableName(),
database.And(columns...),
)
}
func (s session) joinMetadata() database.QueryOption {
columns := make([]database.Condition, 0, 3)
columns = append(columns,
database.NewColumnCondition(s.InstanceIDColumn(), s.metadataRepo.instanceIDColumn()),
database.NewColumnCondition(s.IDColumn(), s.metadataRepo.sessionIDColumn()),
)
return database.WithLeftJoin(
s.metadataRepo.qualifiedTableName(),
database.And(columns...),
)
}
@@ -0,0 +1,190 @@
package repository
import (
"encoding/json"
"time"
"github.com/zitadel/zitadel/backend/v3/domain"
"github.com/zitadel/zitadel/backend/v3/storage/database"
)
type sessionFactor struct {
}
func (s sessionFactor) qualifiedTableName() string {
return "zitadel.session_factors"
}
func (s sessionFactor) unqualifiedTableName() string {
return "session_factors"
}
func (s sessionFactor) PrimaryKeyColumns() []database.Column {
return []database.Column{
s.instanceIDColumn(),
s.sessionIDColumn(),
s.typeColumn(),
}
}
func (s sessionFactor) instanceIDColumn() database.Column {
return database.NewColumn(s.unqualifiedTableName(), "instance_id")
}
func (s sessionFactor) sessionIDColumn() database.Column {
return database.NewColumn(s.unqualifiedTableName(), "session_id")
}
func (s sessionFactor) typeColumn() database.Column {
return database.NewColumn(s.unqualifiedTableName(), "type")
}
func (s sessionFactor) lastVerifiedAtColumn() database.Column {
return database.NewColumn(s.unqualifiedTableName(), "last_verified_at")
}
// -------------------------------------------------------------
// conditions
// -------------------------------------------------------------
func (s sessionFactor) FactorTypeCondition(factorType domain.SessionFactorType) database.Condition {
return database.NewNumberCondition(s.typeColumn(), database.NumberOperationEqual, factorType)
}
func (s sessionFactor) LastVerifiedBeforeCondition(lastVerifiedAt time.Time) database.Condition {
return database.NewNumberCondition(s.lastVerifiedAtColumn(), database.NumberOperationLessThan, lastVerifiedAt)
}
// -------------------------------------------------------------
// scanners
// -------------------------------------------------------------
type rawFactor struct {
Type string `json:"type" db:"type"`
LastChallengedAt time.Time `json:"lastChallengedAt" db:"last_challenged_at"`
ChallengedPayload JSON[json.RawMessage] `json:"challengedPayload" db:"challenged_payload"`
LastVerifiedAt time.Time `json:"lastVerifiedAt" db:"last_verified_at"`
VerifiedPayload JSON[json.RawMessage] `json:"verifiedPayload" db:"verified_payload"`
}
func (f *rawFactor) ToDomain() (domain.SessionFactor, domain.SessionChallenge, error) {
factorType, err := domain.SessionFactorTypeString(f.Type)
if err != nil {
return nil, nil, err
}
switch factorType {
case domain.SessionFactorTypeUser:
return f.userFactorToDomain()
case domain.SessionFactorTypePassword:
return f.passwordFactorToDomain()
case domain.SessionFactorTypePasskey:
return f.passkeyFactorToDomain()
case domain.SessionFactorTypeIdentityProviderIntent:
return f.identityProviderIntentFactorToDomain()
case domain.SessionFactorTypeTOTP:
return f.totpFactorToDomain()
case domain.SessionFactorTypeOTPSMS:
return f.otpSMSFactorToDomain()
case domain.SessionFactorTypeOTPEmail:
return f.otpEmailFactorToDomain()
case domain.SessionFactorTypeRecoveryCode:
return f.recoveryCodeFactorToDomain()
case domain.SessionFactorTypeUnknown:
return nil, nil, nil
default:
return nil, nil, nil
}
}
func (f *rawFactor) userFactorToDomain() (factor domain.SessionFactor, _ domain.SessionChallenge, err error) {
if f.LastVerifiedAt.IsZero() {
return nil, nil, nil
}
factor = new(domain.SessionFactorUser)
if err := json.Unmarshal(f.VerifiedPayload.Value, factor); err != nil {
return nil, nil, err
}
return factor, nil, nil
}
func (f *rawFactor) passwordFactorToDomain() (domain.SessionFactor, domain.SessionChallenge, error) {
if f.LastVerifiedAt.IsZero() {
return nil, nil, nil
}
return &domain.SessionFactorPassword{
LastVerifiedAt: f.LastVerifiedAt,
}, nil, nil
}
func (f *rawFactor) passkeyFactorToDomain() (factor domain.SessionFactor, challenge domain.SessionChallenge, err error) {
if !f.LastChallengedAt.IsZero() && f.LastChallengedAt.After(f.LastVerifiedAt) {
challenge = new(domain.SessionChallengePasskey)
if err := json.Unmarshal(f.ChallengedPayload.Value, &challenge); err != nil {
return nil, nil, err
}
}
if !f.LastVerifiedAt.IsZero() {
factor = new(domain.SessionFactorPasskey)
if err := json.Unmarshal(f.VerifiedPayload.Value, factor); err != nil {
return nil, nil, err
}
}
return factor, challenge, nil
}
func (f *rawFactor) identityProviderIntentFactorToDomain() (domain.SessionFactor, domain.SessionChallenge, error) {
if f.LastVerifiedAt.IsZero() {
return nil, nil, nil
}
return &domain.SessionFactorIdentityProviderIntent{
LastVerifiedAt: f.LastVerifiedAt,
}, nil, nil
}
func (f *rawFactor) totpFactorToDomain() (domain.SessionFactor, domain.SessionChallenge, error) {
if f.LastVerifiedAt.IsZero() {
return nil, nil, nil
}
return &domain.SessionFactorTOTP{
LastVerifiedAt: f.LastVerifiedAt,
}, nil, nil
}
func (f *rawFactor) otpSMSFactorToDomain() (factor domain.SessionFactor, challenge domain.SessionChallenge, err error) {
if !f.LastChallengedAt.IsZero() && f.LastChallengedAt.After(f.LastVerifiedAt) {
challenge = new(domain.SessionChallengeOTPSMS)
if err := json.Unmarshal(f.ChallengedPayload.Value, &challenge); err != nil {
return nil, nil, err
}
}
if !f.LastVerifiedAt.IsZero() {
factor = &domain.SessionFactorOTPSMS{
LastVerifiedAt: f.LastVerifiedAt,
}
}
return factor, challenge, nil
}
func (f *rawFactor) otpEmailFactorToDomain() (factor domain.SessionFactor, challenge domain.SessionChallenge, err error) {
if !f.LastChallengedAt.IsZero() && f.LastChallengedAt.After(f.LastVerifiedAt) {
challenge = new(domain.SessionChallengeOTPEmail)
if err := json.Unmarshal(f.ChallengedPayload.Value, &challenge); err != nil {
return nil, nil, err
}
}
if !f.LastVerifiedAt.IsZero() {
factor = &domain.SessionFactorOTPEmail{
LastVerifiedAt: f.LastVerifiedAt,
}
}
return factor, challenge, nil
}
func (f *rawFactor) recoveryCodeFactorToDomain() (domain.SessionFactor, domain.SessionChallenge, error) {
if f.LastVerifiedAt.IsZero() {
return nil, nil, nil
}
return &domain.SessionFactorRecoveryCode{
LastVerifiedAt: f.LastVerifiedAt,
}, nil, nil
}
@@ -0,0 +1,71 @@
package repository
import "github.com/zitadel/zitadel/backend/v3/storage/database"
type sessionMetadata struct {
}
func (s sessionMetadata) qualifiedTableName() string {
return "zitadel.session_metadata"
}
func (s sessionMetadata) unqualifiedTableName() string {
return "session_metadata"
}
func (s sessionMetadata) PrimaryKeyColumns() []database.Column {
return []database.Column{
s.instanceIDColumn(),
s.sessionIDColumn(),
s.keyColumn(),
}
}
func (s sessionMetadata) instanceIDColumn() database.Column {
return database.NewColumn(s.unqualifiedTableName(), "instance_id")
}
func (s sessionMetadata) sessionIDColumn() database.Column {
return database.NewColumn(s.unqualifiedTableName(), "session_id")
}
func (s sessionMetadata) keyColumn() database.Column {
return database.NewColumn(s.unqualifiedTableName(), "key")
}
func (s sessionMetadata) valueColumn() database.Column {
return database.NewColumn(s.unqualifiedTableName(), "value")
}
// -------------------------------------------------------------
// conditions
// -------------------------------------------------------------
// PrimaryKeyCondition implements [domain.SessionMetadataRepository].
func (s sessionMetadata) PrimaryKeyCondition(instanceID string, sessionID string, key string) database.Condition {
return database.And(
s.InstanceIDCondition(instanceID),
s.SessionIDCondition(sessionID),
s.KeyCondition(database.TextOperationEqual, key),
)
}
// InstanceIDCondition implements [domain.SessionMetadataRepository].
func (s sessionMetadata) InstanceIDCondition(instanceID string) database.Condition {
return database.NewTextCondition(s.instanceIDColumn(), database.TextOperationEqual, instanceID)
}
// SessionIDCondition implements [domain.SessionMetadataRepository].
func (s sessionMetadata) SessionIDCondition(sessionID string) database.Condition {
return database.NewTextCondition(s.sessionIDColumn(), database.TextOperationEqual, sessionID)
}
// KeyCondition implements [domain.SessionMetadataRepository].
func (s sessionMetadata) KeyCondition(op database.TextOperation, key string) database.Condition {
return database.NewTextCondition(s.keyColumn(), op, key)
}
// ValueCondition implements [domain.SessionMetadataRepository].
func (s sessionMetadata) ValueCondition(op database.BytesOperation, value []byte) database.Condition {
return database.NewBytesCondition[[]byte](database.SHA256Column(s.valueColumn()), op, database.SHA256Value(value))
}
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,43 @@
package repository
import (
"github.com/zitadel/zitadel/backend/v3/storage/database"
)
type sessionUserAgent struct {
}
func (s sessionUserAgent) qualifiedTableName() string {
return "zitadel.session_user_agents"
}
func (s sessionUserAgent) unqualifiedTableName() string {
return "session_user_agents"
}
func (s sessionUserAgent) PrimaryKeyColumns() []database.Column {
return []database.Column{
s.instanceIDColumn(),
s.fingerprintIDColumn(),
}
}
func (s sessionUserAgent) instanceIDColumn() database.Column {
return database.NewColumn(s.unqualifiedTableName(), "instance_id")
}
func (s sessionUserAgent) fingerprintIDColumn() database.Column {
return database.NewColumn(s.unqualifiedTableName(), "fingerprint_id")
}
func (s sessionUserAgent) descriptionColumn() database.Column {
return database.NewColumn(s.unqualifiedTableName(), "description")
}
func (s sessionUserAgent) ipColumn() database.Column {
return database.NewColumn(s.unqualifiedTableName(), "ip")
}
func (s sessionUserAgent) headersColumn() database.Column {
return database.NewColumn(s.unqualifiedTableName(), "headers")
}
+3
View File
@@ -99,6 +99,7 @@ var (
OrganizationMetadataRelationalProjection *handler.Handler
AuthorizationRelationalProjection *handler.Handler
ProjectGrantRelationalProjection *handler.Handler
SessionRelationalProjection *handler.Handler
SettingsRelationalProjection *handler.Handler
ProjectGrantFields *handler.FieldHandler
@@ -228,6 +229,7 @@ func Create(ctx context.Context, sqlClient *database.DB, es handler.EventStore,
OrganizationMetadataRelationalProjection = newOrgMetadataRelationalProjection(ctx, applyCustomConfig(projectionConfig, config.Customizations["organization_metadata_relational"]))
AuthorizationRelationalProjection = newAuthorizationRelationalProjection(ctx, applyCustomConfig(projectionConfig, config.Customizations["authorizations_relational"]))
ProjectGrantRelationalProjection = newProjectGrantRelationalProjection(ctx, applyCustomConfig(projectionConfig, config.Customizations["project_grant_relational"]))
SessionRelationalProjection = newSessionRelationalProjection(ctx, applyCustomConfig(projectionConfig, config.Customizations["session_relational"]))
newProjectionsList()
newFieldsList()
@@ -424,5 +426,6 @@ func newProjectionsList() {
OrganizationMetadataRelationalProjection,
AuthorizationRelationalProjection,
ProjectGrantRelationalProjection,
SessionRelationalProjection,
}
}
@@ -0,0 +1,575 @@
package projection
import (
"context"
"database/sql"
"github.com/zitadel/zitadel/backend/v3/domain"
"github.com/zitadel/zitadel/backend/v3/storage/database"
v3_sql "github.com/zitadel/zitadel/backend/v3/storage/database/dialect/sql"
"github.com/zitadel/zitadel/backend/v3/storage/database/repository"
"github.com/zitadel/zitadel/internal/eventstore"
"github.com/zitadel/zitadel/internal/eventstore/handler/v2"
"github.com/zitadel/zitadel/internal/repository/instance"
"github.com/zitadel/zitadel/internal/repository/session"
"github.com/zitadel/zitadel/internal/repository/user"
"github.com/zitadel/zitadel/internal/zerrors"
)
const (
SessionsRelationalProjectionTable = "zitadel.sessions"
)
type sessionRelationalProjection struct{}
func (*sessionRelationalProjection) Name() string {
return SessionsRelationalProjectionTable
}
func newSessionRelationalProjection(ctx context.Context, config handler.Config) *handler.Handler {
return handler.NewHandler(ctx, &config, new(sessionRelationalProjection))
}
func (p *sessionRelationalProjection) Reducers() []handler.AggregateReducer {
return []handler.AggregateReducer{
{
Aggregate: session.AggregateType,
EventReducers: []handler.EventReducer{
{
Event: session.AddedType,
Reduce: p.reduceSessionAdded,
},
{
Event: session.UserCheckedType,
Reduce: p.reduceUserChecked,
},
{
Event: session.PasswordCheckedType,
Reduce: p.reducePasswordChecked,
},
{
Event: session.IntentCheckedType,
Reduce: p.reduceIntentChecked,
},
{
Event: session.WebAuthNChallengedType,
Reduce: p.reduceWebAuthNChallenged,
},
{
Event: session.WebAuthNCheckedType,
Reduce: p.reduceWebAuthNChecked,
},
{
Event: session.TOTPCheckedType,
Reduce: p.reduceTOTPChecked,
},
{
Event: session.OTPSMSChallengedType,
Reduce: p.reduceOTPSMSChallenged,
},
{
Event: session.OTPSMSCheckedType,
Reduce: p.reduceOTPSMSChecked,
},
{
Event: session.OTPEmailChallengedType,
Reduce: p.reduceOTPEmailChallenged,
},
{
Event: session.OTPEmailCheckedType,
Reduce: p.reduceOTPEmailChecked,
},
{
Event: session.RecoveryCodeCheckedType,
Reduce: p.reduceRecoveryCodeChecked,
},
{
Event: session.TokenSetType,
Reduce: p.reduceTokenSet,
},
{
Event: session.MetadataSetType,
Reduce: p.reduceMetadataSet,
},
{
Event: session.LifetimeSetType,
Reduce: p.reduceLifetimeSet,
},
{
Event: session.TerminateType,
Reduce: p.reduceSessionTerminated,
},
},
},
{
Aggregate: instance.AggregateType,
EventReducers: []handler.EventReducer{
{
Event: instance.InstanceRemovedEventType,
Reduce: reduceInstanceRemovedHelper(SMSColumnInstanceID),
},
},
},
{
Aggregate: user.AggregateType,
EventReducers: []handler.EventReducer{
{
Event: user.HumanPasswordChangedType,
Reduce: p.reducePasswordChanged,
},
},
},
}
}
func (p *sessionRelationalProjection) reduceSessionAdded(event eventstore.Event) (*handler.Statement, error) {
e, err := assertEvent[*session.AddedEvent](event)
if err != nil {
return nil, err
}
return handler.NewStatement(e, func(ctx context.Context, ex handler.Executer, projectionName string) error {
tx, ok := ex.(*sql.Tx)
if !ok {
return zerrors.ThrowInvalidArgumentf(nil, "HANDL-iZGH3", "reduce.wrong.db.pool %T", ex)
}
v3Tx := v3_sql.SQLTx(tx)
var userAgent *domain.SessionUserAgent
if e.UserAgent != nil {
userAgent = &domain.SessionUserAgent{
FingerprintID: e.UserAgent.FingerprintID,
Description: e.UserAgent.Description,
IP: e.UserAgent.IP,
Header: e.UserAgent.Header,
}
}
sessionRepo := repository.SessionRepository()
return sessionRepo.Create(ctx, v3Tx, &domain.Session{
InstanceID: e.Aggregate().InstanceID,
ID: e.Aggregate().ID,
CreatorID: e.User,
CreatedAt: e.CreationDate(),
UpdatedAt: e.CreationDate(),
UserAgent: userAgent,
})
}), nil
}
func (p *sessionRelationalProjection) reduceUserChecked(event eventstore.Event) (*handler.Statement, error) {
e, err := assertEvent[*session.UserCheckedEvent](event)
if err != nil {
return nil, err
}
return handler.NewStatement(e, func(ctx context.Context, ex handler.Executer, projectionName string) error {
tx, ok := ex.(*sql.Tx)
if !ok {
return zerrors.ThrowInvalidArgumentf(nil, "HANDL-UHE92", "reduce.wrong.db.pool %T", ex)
}
v3Tx := v3_sql.SQLTx(tx)
sessionRepo := repository.SessionRepository()
condition := sessionRepo.PrimaryKeyCondition(e.Aggregate().InstanceID, e.Aggregate().ID)
_, err = sessionRepo.Update(ctx, v3Tx, condition,
sessionRepo.SetUpdatedAt(e.CreatedAt()),
sessionRepo.SetFactor(&domain.SessionFactorUser{
UserID: e.UserID,
LastVerifiedAt: e.CreatedAt(),
}),
)
return err
}), nil
}
func (p *sessionRelationalProjection) reducePasswordChecked(event eventstore.Event) (*handler.Statement, error) {
e, err := assertEvent[*session.PasswordCheckedEvent](event)
if err != nil {
return nil, err
}
return handler.NewStatement(e, func(ctx context.Context, ex handler.Executer, projectionName string) error {
tx, ok := ex.(*sql.Tx)
if !ok {
return zerrors.ThrowInvalidArgumentf(nil, "HANDL-3krfa", "reduce.wrong.db.pool %T", ex)
}
v3Tx := v3_sql.SQLTx(tx)
sessionRepo := repository.SessionRepository()
condition := sessionRepo.PrimaryKeyCondition(e.Aggregate().InstanceID, e.Aggregate().ID)
_, err = sessionRepo.Update(ctx, v3Tx, condition,
sessionRepo.SetUpdatedAt(e.CreatedAt()),
sessionRepo.SetFactor(&domain.SessionFactorPassword{
LastVerifiedAt: e.CreatedAt(),
}),
)
return err
}), nil
}
func (p *sessionRelationalProjection) reduceIntentChecked(event eventstore.Event) (*handler.Statement, error) {
e, err := assertEvent[*session.IntentCheckedEvent](event)
if err != nil {
return nil, err
}
return handler.NewStatement(e, func(ctx context.Context, ex handler.Executer, projectionName string) error {
tx, ok := ex.(*sql.Tx)
if !ok {
return zerrors.ThrowInvalidArgumentf(nil, "HANDL-ajkd2", "reduce.wrong.db.pool %T", ex)
}
v3Tx := v3_sql.SQLTx(tx)
sessionRepo := repository.SessionRepository()
condition := sessionRepo.PrimaryKeyCondition(e.Aggregate().InstanceID, e.Aggregate().ID)
_, err = sessionRepo.Update(ctx, v3Tx, condition,
sessionRepo.SetUpdatedAt(e.CreatedAt()),
sessionRepo.SetFactor(&domain.SessionFactorIdentityProviderIntent{
LastVerifiedAt: e.CreatedAt(),
}),
)
return err
}), nil
}
func (p *sessionRelationalProjection) reduceWebAuthNChallenged(event eventstore.Event) (*handler.Statement, error) {
e, err := assertEvent[*session.WebAuthNChallengedEvent](event)
if err != nil {
return nil, err
}
return handler.NewStatement(e, func(ctx context.Context, ex handler.Executer, projectionName string) error {
tx, ok := ex.(*sql.Tx)
if !ok {
return zerrors.ThrowInvalidArgumentf(nil, "HANDL-do35d", "reduce.wrong.db.pool %T", ex)
}
v3Tx := v3_sql.SQLTx(tx)
sessionRepo := repository.SessionRepository()
condition := sessionRepo.PrimaryKeyCondition(e.Aggregate().InstanceID, e.Aggregate().ID)
_, err = sessionRepo.Update(ctx, v3Tx, condition,
sessionRepo.SetUpdatedAt(e.CreatedAt()),
sessionRepo.SetChallenge(&domain.SessionChallengePasskey{
LastChallengedAt: e.CreatedAt(),
Challenge: e.Challenge,
AllowedCredentialIDs: e.AllowedCrentialIDs,
UserVerification: e.UserVerification,
RPID: e.RPID,
}),
)
return err
}), nil
}
func (p *sessionRelationalProjection) reduceWebAuthNChecked(event eventstore.Event) (*handler.Statement, error) {
e, err := assertEvent[*session.WebAuthNCheckedEvent](event)
if err != nil {
return nil, err
}
return handler.NewStatement(e, func(ctx context.Context, ex handler.Executer, projectionName string) error {
tx, ok := ex.(*sql.Tx)
if !ok {
return zerrors.ThrowInvalidArgumentf(nil, "HANDL-djk22", "reduce.wrong.db.pool %T", ex)
}
v3Tx := v3_sql.SQLTx(tx)
sessionRepo := repository.SessionRepository()
condition := sessionRepo.PrimaryKeyCondition(e.Aggregate().InstanceID, e.Aggregate().ID)
_, err = sessionRepo.Update(ctx, v3Tx, condition,
sessionRepo.SetUpdatedAt(e.CreatedAt()),
sessionRepo.SetFactor(&domain.SessionFactorPasskey{
LastVerifiedAt: e.CreatedAt(),
UserVerified: e.UserVerified,
}),
)
return err
}), nil
}
func (p *sessionRelationalProjection) reduceTOTPChecked(event eventstore.Event) (*handler.Statement, error) {
e, err := assertEvent[*session.TOTPCheckedEvent](event)
if err != nil {
return nil, err
}
return handler.NewStatement(e, func(ctx context.Context, ex handler.Executer, projectionName string) error {
tx, ok := ex.(*sql.Tx)
if !ok {
return zerrors.ThrowInvalidArgumentf(nil, "HANDL-cklr9", "reduce.wrong.db.pool %T", ex)
}
v3Tx := v3_sql.SQLTx(tx)
sessionRepo := repository.SessionRepository()
condition := sessionRepo.PrimaryKeyCondition(e.Aggregate().InstanceID, e.Aggregate().ID)
_, err = sessionRepo.Update(ctx, v3Tx, condition,
sessionRepo.SetUpdatedAt(e.CreatedAt()),
sessionRepo.SetFactor(&domain.SessionFactorTOTP{
LastVerifiedAt: e.CreatedAt(),
}),
)
return err
}), nil
}
func (p *sessionRelationalProjection) reduceOTPSMSChallenged(event eventstore.Event) (*handler.Statement, error) {
e, err := assertEvent[*session.OTPSMSChallengedEvent](event)
if err != nil {
return nil, err
}
return handler.NewStatement(e, func(ctx context.Context, ex handler.Executer, projectionName string) error {
tx, ok := ex.(*sql.Tx)
if !ok {
return zerrors.ThrowInvalidArgumentf(nil, "HANDL-fk4f9", "reduce.wrong.db.pool %T", ex)
}
v3Tx := v3_sql.SQLTx(tx)
sessionRepo := repository.SessionRepository()
condition := sessionRepo.PrimaryKeyCondition(e.Aggregate().InstanceID, e.Aggregate().ID)
_, err = sessionRepo.Update(ctx, v3Tx, condition,
sessionRepo.SetUpdatedAt(e.CreatedAt()),
sessionRepo.SetChallenge(&domain.SessionChallengeOTPSMS{
LastChallengedAt: e.CreatedAt(),
Code: e.Code,
Expiry: e.Expiry,
CodeReturned: e.CodeReturned,
GeneratorID: e.GeneratorID,
TriggeredAtOrigin: e.TriggeredAtOrigin,
}),
)
return err
}), nil
}
func (p *sessionRelationalProjection) reduceOTPSMSChecked(event eventstore.Event) (*handler.Statement, error) {
e, err := assertEvent[*session.OTPSMSCheckedEvent](event)
if err != nil {
return nil, err
}
return handler.NewStatement(e, func(ctx context.Context, ex handler.Executer, projectionName string) error {
tx, ok := ex.(*sql.Tx)
if !ok {
return zerrors.ThrowInvalidArgumentf(nil, "HANDL-dk39f", "reduce.wrong.db.pool %T", ex)
}
v3Tx := v3_sql.SQLTx(tx)
sessionRepo := repository.SessionRepository()
condition := sessionRepo.PrimaryKeyCondition(e.Aggregate().InstanceID, e.Aggregate().ID)
_, err = sessionRepo.Update(ctx, v3Tx, condition,
sessionRepo.SetUpdatedAt(e.CreatedAt()),
sessionRepo.SetFactor(&domain.SessionFactorOTPSMS{
LastVerifiedAt: e.CreatedAt(),
}),
)
return err
}), nil
}
func (p *sessionRelationalProjection) reduceOTPEmailChallenged(event eventstore.Event) (*handler.Statement, error) {
e, err := assertEvent[*session.OTPEmailChallengedEvent](event)
if err != nil {
return nil, err
}
return handler.NewStatement(e, func(ctx context.Context, ex handler.Executer, projectionName string) error {
tx, ok := ex.(*sql.Tx)
if !ok {
return zerrors.ThrowInvalidArgumentf(nil, "HANDL-fkf93", "reduce.wrong.db.pool %T", ex)
}
v3Tx := v3_sql.SQLTx(tx)
sessionRepo := repository.SessionRepository()
condition := sessionRepo.PrimaryKeyCondition(e.Aggregate().InstanceID, e.Aggregate().ID)
_, err = sessionRepo.Update(ctx, v3Tx, condition,
sessionRepo.SetUpdatedAt(e.CreatedAt()),
sessionRepo.SetChallenge(&domain.SessionChallengeOTPEmail{
LastChallengedAt: e.CreatedAt(),
Code: e.Code,
Expiry: e.Expiry,
CodeReturned: e.ReturnCode,
URLTemplate: e.URLTmpl,
TriggeredAtOrigin: e.TriggeredAtOrigin,
}),
)
return err
}), nil
}
func (p *sessionRelationalProjection) reduceOTPEmailChecked(event eventstore.Event) (*handler.Statement, error) {
e, err := assertEvent[*session.OTPEmailCheckedEvent](event)
if err != nil {
return nil, err
}
return handler.NewStatement(e, func(ctx context.Context, ex handler.Executer, projectionName string) error {
tx, ok := ex.(*sql.Tx)
if !ok {
return zerrors.ThrowInvalidArgumentf(nil, "HANDL-3kll0", "reduce.wrong.db.pool %T", ex)
}
v3Tx := v3_sql.SQLTx(tx)
sessionRepo := repository.SessionRepository()
condition := sessionRepo.PrimaryKeyCondition(e.Aggregate().InstanceID, e.Aggregate().ID)
_, err = sessionRepo.Update(ctx, v3Tx, condition,
sessionRepo.SetUpdatedAt(e.CreatedAt()),
sessionRepo.SetFactor(&domain.SessionFactorOTPEmail{
LastVerifiedAt: e.CreatedAt(),
}),
)
return err
}), nil
}
func (p *sessionRelationalProjection) reduceRecoveryCodeChecked(event eventstore.Event) (*handler.Statement, error) {
e, err := assertEvent[*session.RecoveryCodeCheckedEvent](event)
if err != nil {
return nil, err
}
return handler.NewStatement(e, func(ctx context.Context, ex handler.Executer, projectionName string) error {
tx, ok := ex.(*sql.Tx)
if !ok {
return zerrors.ThrowInvalidArgumentf(nil, "HANDL-fk45a", "reduce.wrong.db.pool %T", ex)
}
v3Tx := v3_sql.SQLTx(tx)
sessionRepo := repository.SessionRepository()
condition := sessionRepo.PrimaryKeyCondition(e.Aggregate().InstanceID, e.Aggregate().ID)
_, err = sessionRepo.Update(ctx, v3Tx, condition,
sessionRepo.SetUpdatedAt(e.CreatedAt()),
sessionRepo.SetFactor(&domain.SessionFactorRecoveryCode{
LastVerifiedAt: e.CreatedAt(),
}),
)
return err
}), nil
}
func (p *sessionRelationalProjection) reduceTokenSet(event eventstore.Event) (*handler.Statement, error) {
e, err := assertEvent[*session.TokenSetEvent](event)
if err != nil {
return nil, err
}
return handler.NewStatement(e, func(ctx context.Context, ex handler.Executer, projectionName string) error {
tx, ok := ex.(*sql.Tx)
if !ok {
return zerrors.ThrowInvalidArgumentf(nil, "HANDL-asddt", "reduce.wrong.db.pool %T", ex)
}
v3Tx := v3_sql.SQLTx(tx)
sessionRepo := repository.SessionRepository()
condition := sessionRepo.PrimaryKeyCondition(e.Aggregate().InstanceID, e.Aggregate().ID)
_, err = sessionRepo.Update(ctx, v3Tx, condition,
sessionRepo.SetUpdatedAt(e.CreatedAt()),
sessionRepo.SetToken(e.TokenID),
)
return err
}), nil
}
func (p *sessionRelationalProjection) reduceMetadataSet(event eventstore.Event) (*handler.Statement, error) {
e, err := assertEvent[*session.MetadataSetEvent](event)
if err != nil {
return nil, err
}
metadataList := make([]*domain.SessionMetadata, 0, len(e.Metadata))
for key, value := range e.Metadata {
metadataList = append(metadataList, &domain.SessionMetadata{
Metadata: domain.Metadata{
Key: key,
Value: value,
},
})
}
return handler.NewStatement(e, func(ctx context.Context, ex handler.Executer, projectionName string) error {
tx, ok := ex.(*sql.Tx)
if !ok {
return zerrors.ThrowInvalidArgumentf(nil, "HANDL-iZGH3", "reduce.wrong.db.pool %T", ex)
}
v3Tx := v3_sql.SQLTx(tx)
sessionRepo := repository.SessionRepository()
condition := sessionRepo.PrimaryKeyCondition(e.Aggregate().InstanceID, e.Aggregate().ID)
_, err = sessionRepo.Update(ctx, v3Tx, condition,
sessionRepo.SetUpdatedAt(e.CreatedAt()),
sessionRepo.SetMetadata(metadataList),
)
return err
}), nil
}
func (p *sessionRelationalProjection) reduceLifetimeSet(event eventstore.Event) (*handler.Statement, error) {
e, err := assertEvent[*session.LifetimeSetEvent](event)
if err != nil {
return nil, err
}
return handler.NewStatement(e, func(ctx context.Context, ex handler.Executer, projectionName string) error {
tx, ok := ex.(*sql.Tx)
if !ok {
return zerrors.ThrowInvalidArgumentf(nil, "HANDL-iZGH3", "reduce.wrong.db.pool %T", ex)
}
v3Tx := v3_sql.SQLTx(tx)
sessionRepo := repository.SessionRepository()
condition := sessionRepo.PrimaryKeyCondition(e.Aggregate().InstanceID, e.Aggregate().ID)
_, err = sessionRepo.Update(ctx, v3Tx, condition,
sessionRepo.SetUpdatedAt(e.CreatedAt()),
sessionRepo.SetLifetime(e.Lifetime),
)
return err
}), nil
}
func (p *sessionRelationalProjection) reduceSessionTerminated(event eventstore.Event) (*handler.Statement, error) {
e, err := assertEvent[*session.TerminateEvent](event)
if err != nil {
return nil, err
}
return handler.NewStatement(e, func(ctx context.Context, ex handler.Executer, projectionName string) error {
tx, ok := ex.(*sql.Tx)
if !ok {
return zerrors.ThrowInvalidArgumentf(nil, "HANDL-iZGH3", "reduce.wrong.db.pool %T", ex)
}
v3Tx := v3_sql.SQLTx(tx)
sessionRepo := repository.SessionRepository()
condition := sessionRepo.PrimaryKeyCondition(e.Aggregate().InstanceID, e.Aggregate().ID)
_, err := sessionRepo.Delete(ctx, v3Tx, condition)
return err
}), nil
}
func (p *sessionRelationalProjection) reducePasswordChanged(event eventstore.Event) (*handler.Statement, error) {
e, err := assertEvent[*user.HumanPasswordChangedEvent](event)
if err != nil {
return nil, err
}
return handler.NewStatement(e, func(ctx context.Context, ex handler.Executer, projectionName string) error {
tx, ok := ex.(*sql.Tx)
if !ok {
return zerrors.ThrowInvalidArgumentf(nil, "HANDL-iZGH3", "reduce.wrong.db.pool %T", ex)
}
v3Tx := v3_sql.SQLTx(tx)
sessionRepo := repository.SessionRepository()
condition := database.And(
sessionRepo.InstanceIDCondition(e.Aggregate().InstanceID),
sessionRepo.UserIDCondition(e.Aggregate().ID),
sessionRepo.ExistsFactor(
database.And(
sessionRepo.FactorConditions().FactorTypeCondition(domain.SessionFactorTypePassword),
sessionRepo.FactorConditions().LastVerifiedBeforeCondition(e.CreatedAt()),
),
),
)
_, err = sessionRepo.Update(ctx, v3Tx, condition,
sessionRepo.SetUpdatedAt(e.CreatedAt()),
sessionRepo.ClearFactor(domain.SessionFactorTypePassword),
)
return err
}), nil
}
+4
View File
@@ -80,3 +80,7 @@ func (c *Client) CreateAssertionResponse(optionsPb *structpb.Struct, verifyUser
}
return resp, nil
}
func (c *Client) KeyID() []byte {
return c.credential.ID
}