mirror of
https://github.com/andwati/zitadel.git
synced 2026-11-07 06:42:15 +00:00
feat: add delete session on relation tables (#11334)
# Which Problems Are Solved As part of moving the session API to the relational database, this PR adds the functionality to delete sessions through the session API v2 and the OIDC end_session endpoint. # How the Problems Are Solved - added `SessionDeleteCommand` in domain package - added possibility to pass the session token verifier to the domain package - use feature flag to switch API and OIDC usage from CQRS to relation table - added a `permissionCheck` condition to let permissions be checked directly in the repository / sql - part of it is still a placeholder until actual permission tables are done - added a `session_deleted` table to store (recently) deleted sessions to handle necessary checks / cases like delete my own session, which needs to be idempotent # Additional Changes Fixed `Matches` function of `existsCondition` (as missing an implementation) # Additional Context - closes #11037 --------- Co-authored-by: Stefan Benz <[email protected]>
This commit is contained in:
co-authored by
Stefan Benz
parent
ae32508225
commit
ca4bba3992
@@ -0,0 +1,34 @@
|
||||
package v2
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"connectrpc.com/connect"
|
||||
"google.golang.org/protobuf/types/known/timestamppb"
|
||||
|
||||
"github.com/zitadel/zitadel/backend/v3/domain"
|
||||
"github.com/zitadel/zitadel/backend/v3/storage/database/repository"
|
||||
"github.com/zitadel/zitadel/pkg/grpc/object/v2"
|
||||
"github.com/zitadel/zitadel/pkg/grpc/session/v2"
|
||||
)
|
||||
|
||||
func DeleteSession(ctx context.Context, request *connect.Request[session.DeleteSessionRequest]) (*connect.Response[session.DeleteSessionResponse], error) {
|
||||
sessionDeleteCmd := domain.NewDeleteSessionCommand(request.Msg.GetSessionId(), request.Msg.GetSessionToken(), true)
|
||||
|
||||
err := domain.Invoke(ctx, sessionDeleteCmd,
|
||||
domain.WithSessionRepo(repository.SessionRepository()),
|
||||
)
|
||||
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
details := &object.Details{}
|
||||
if !sessionDeleteCmd.DeletedAt.IsZero() {
|
||||
details.ChangeDate = timestamppb.New(sessionDeleteCmd.DeletedAt)
|
||||
}
|
||||
|
||||
return connect.NewResponse(&session.DeleteSessionResponse{
|
||||
Details: details,
|
||||
}), nil
|
||||
}
|
||||
@@ -9,12 +9,11 @@ import (
|
||||
|
||||
var (
|
||||
pool database.Pool
|
||||
// tracer tracing.Tracer = tracing.GlobalTracer()
|
||||
// runtimeLogger *slog.Logger = logging.New(logging.StreamRuntime, nil)
|
||||
legacyEventstore eventstore.LegacyEventstore
|
||||
sysConfig systemdefaults.SystemDefaults
|
||||
passwordHasher *crypto.Hasher
|
||||
idpEncryptionAlgo crypto.EncryptionAlgorithm
|
||||
sessionTokenDecryptor SessionTokenDecryptor
|
||||
)
|
||||
|
||||
func SetPool(p database.Pool) {
|
||||
@@ -36,3 +35,7 @@ func SetPasswordHasher(hasher *crypto.Hasher) {
|
||||
func SetIDPEncryptionAlgorithm(idpEncryptionAlg crypto.EncryptionAlgorithm) {
|
||||
idpEncryptionAlgo = idpEncryptionAlg
|
||||
}
|
||||
|
||||
func SetSessionTokenDecryptor(decryptor SessionTokenDecryptor) {
|
||||
sessionTokenDecryptor = decryptor
|
||||
}
|
||||
|
||||
@@ -87,17 +87,27 @@ func WithLegacyEventstore(es eventstore.LegacyEventstore) InvokeOpt {
|
||||
}
|
||||
}
|
||||
|
||||
// WithSessionTokenDecryptor sets the decryptor for session tokens used by the commands.
|
||||
// If not set, the default one will be used.
|
||||
// This is mainly used for testing
|
||||
func WithSessionTokenDecryptor(decryptor SessionTokenDecryptor) InvokeOpt {
|
||||
return func(opts *InvokeOpts) {
|
||||
opts.sessionTokenDecryptor = decryptor
|
||||
}
|
||||
}
|
||||
|
||||
// InvokeOpts are passed to each command
|
||||
type InvokeOpts struct {
|
||||
// db is the database client.
|
||||
// [Executor]s MUST NOT access this field directly, use [InvokeOpts.DB] to access it.
|
||||
//
|
||||
// [Invoker]s may manipulate this field for example changing it to a transaction.
|
||||
// Its their responsibility to restore it after ending the transaction.
|
||||
// It's their responsibility to restore it after ending the transaction.
|
||||
db database.QueryExecutor
|
||||
legacyEventstore eventstore.LegacyEventstore
|
||||
Invoker Invoker
|
||||
Permissions PermissionChecker
|
||||
sessionTokenDecryptor SessionTokenDecryptor
|
||||
organizationRepo OrganizationRepository
|
||||
organizationDomainRepo OrganizationDomainRepository
|
||||
projectRepo ProjectRepository
|
||||
@@ -139,7 +149,8 @@ func DefaultOpts(invoker Invoker) *InvokeOpts {
|
||||
invoker = &noopInvoker{}
|
||||
}
|
||||
return &InvokeOpts{
|
||||
Invoker: invoker,
|
||||
Permissions: &noopPermissionChecker{}, // prevent panics for now
|
||||
Invoker: invoker,
|
||||
Permissions: &noopPermissionChecker{}, // prevent panics for now
|
||||
sessionTokenDecryptor: sessionTokenDecryptor,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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}
|
||||
}
|
||||
|
||||
@@ -196,17 +195,17 @@ func (c *MockSessionRepositoryCreatedAtConditionCall) DoAndReturn(f func(databas
|
||||
}
|
||||
|
||||
// CreatorIDCondition mocks base method.
|
||||
func (m *MockSessionRepository) CreatorIDCondition(creatorID string) database.Condition {
|
||||
func (m *MockSessionRepository) CreatorIDCondition(arg0 string) database.Condition {
|
||||
m.ctrl.T.Helper()
|
||||
ret := m.ctrl.Call(m, "CreatorIDCondition", creatorID)
|
||||
ret := m.ctrl.Call(m, "CreatorIDCondition", arg0)
|
||||
ret0, _ := ret[0].(database.Condition)
|
||||
return ret0
|
||||
}
|
||||
|
||||
// CreatorIDCondition indicates an expected call of CreatorIDCondition.
|
||||
func (mr *MockSessionRepositoryMockRecorder) CreatorIDCondition(creatorID any) *MockSessionRepositoryCreatorIDConditionCall {
|
||||
func (mr *MockSessionRepositoryMockRecorder) CreatorIDCondition(arg0 any) *MockSessionRepositoryCreatorIDConditionCall {
|
||||
mr.mock.ctrl.T.Helper()
|
||||
call := mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "CreatorIDCondition", reflect.TypeOf((*MockSessionRepository)(nil).CreatorIDCondition), creatorID)
|
||||
call := mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "CreatorIDCondition", reflect.TypeOf((*MockSessionRepository)(nil).CreatorIDCondition), arg0)
|
||||
return &MockSessionRepositoryCreatorIDConditionCall{Call: call}
|
||||
}
|
||||
|
||||
@@ -234,18 +233,19 @@ func (c *MockSessionRepositoryCreatorIDConditionCall) DoAndReturn(f func(string)
|
||||
}
|
||||
|
||||
// Delete mocks base method.
|
||||
func (m *MockSessionRepository) Delete(ctx context.Context, client database.QueryExecutor, condition database.Condition) (int64, error) {
|
||||
func (m *MockSessionRepository) Delete(arg0 context.Context, arg1 database.QueryExecutor, arg2, arg3 database.Condition) (int64, time.Time, error) {
|
||||
m.ctrl.T.Helper()
|
||||
ret := m.ctrl.Call(m, "Delete", ctx, client, condition)
|
||||
ret := m.ctrl.Call(m, "Delete", arg0, arg1, arg2, arg3)
|
||||
ret0, _ := ret[0].(int64)
|
||||
ret1, _ := ret[1].(error)
|
||||
return ret0, ret1
|
||||
ret1, _ := ret[1].(time.Time)
|
||||
ret2, _ := ret[2].(error)
|
||||
return ret0, ret1, ret2
|
||||
}
|
||||
|
||||
// Delete indicates an expected call of Delete.
|
||||
func (mr *MockSessionRepositoryMockRecorder) Delete(ctx, client, condition any) *MockSessionRepositoryDeleteCall {
|
||||
func (mr *MockSessionRepositoryMockRecorder) Delete(arg0, arg1, arg2, arg3 any) *MockSessionRepositoryDeleteCall {
|
||||
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, "Delete", reflect.TypeOf((*MockSessionRepository)(nil).Delete), arg0, arg1, arg2, arg3)
|
||||
return &MockSessionRepositoryDeleteCall{Call: call}
|
||||
}
|
||||
|
||||
@@ -255,35 +255,35 @@ type MockSessionRepositoryDeleteCall struct {
|
||||
}
|
||||
|
||||
// Return rewrite *gomock.Call.Return
|
||||
func (c *MockSessionRepositoryDeleteCall) Return(arg0 int64, arg1 error) *MockSessionRepositoryDeleteCall {
|
||||
c.Call = c.Call.Return(arg0, arg1)
|
||||
func (c *MockSessionRepositoryDeleteCall) Return(arg0 int64, arg1 time.Time, arg2 error) *MockSessionRepositoryDeleteCall {
|
||||
c.Call = c.Call.Return(arg0, arg1, arg2)
|
||||
return c
|
||||
}
|
||||
|
||||
// Do rewrite *gomock.Call.Do
|
||||
func (c *MockSessionRepositoryDeleteCall) Do(f func(context.Context, database.QueryExecutor, database.Condition) (int64, error)) *MockSessionRepositoryDeleteCall {
|
||||
func (c *MockSessionRepositoryDeleteCall) Do(f func(context.Context, database.QueryExecutor, database.Condition, database.Condition) (int64, time.Time, 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) (int64, error)) *MockSessionRepositoryDeleteCall {
|
||||
func (c *MockSessionRepositoryDeleteCall) DoAndReturn(f func(context.Context, database.QueryExecutor, database.Condition, database.Condition) (int64, time.Time, error)) *MockSessionRepositoryDeleteCall {
|
||||
c.Call = c.Call.DoAndReturn(f)
|
||||
return c
|
||||
}
|
||||
|
||||
// ExistsFactor mocks base method.
|
||||
func (m *MockSessionRepository) ExistsFactor(condition database.Condition) database.Condition {
|
||||
func (m *MockSessionRepository) ExistsFactor(arg0 database.Condition) database.Condition {
|
||||
m.ctrl.T.Helper()
|
||||
ret := m.ctrl.Call(m, "ExistsFactor", condition)
|
||||
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(condition any) *MockSessionRepositoryExistsFactorCall {
|
||||
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), condition)
|
||||
call := mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ExistsFactor", reflect.TypeOf((*MockSessionRepository)(nil).ExistsFactor), arg0)
|
||||
return &MockSessionRepositoryExistsFactorCall{Call: call}
|
||||
}
|
||||
|
||||
@@ -311,17 +311,17 @@ func (c *MockSessionRepositoryExistsFactorCall) DoAndReturn(f func(database.Cond
|
||||
}
|
||||
|
||||
// ExistsMetadata mocks base method.
|
||||
func (m *MockSessionRepository) ExistsMetadata(condition database.Condition) database.Condition {
|
||||
func (m *MockSessionRepository) ExistsMetadata(arg0 database.Condition) database.Condition {
|
||||
m.ctrl.T.Helper()
|
||||
ret := m.ctrl.Call(m, "ExistsMetadata", condition)
|
||||
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(condition any) *MockSessionRepositoryExistsMetadataCall {
|
||||
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), condition)
|
||||
call := mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ExistsMetadata", reflect.TypeOf((*MockSessionRepository)(nil).ExistsMetadata), arg0)
|
||||
return &MockSessionRepositoryExistsMetadataCall{Call: call}
|
||||
}
|
||||
|
||||
@@ -387,17 +387,17 @@ func (c *MockSessionRepositoryExpirationColumnCall) DoAndReturn(f func() databas
|
||||
}
|
||||
|
||||
// ExpirationCondition mocks base method.
|
||||
func (m *MockSessionRepository) ExpirationCondition(op database.NumberOperation, expiration time.Time) database.Condition {
|
||||
func (m *MockSessionRepository) ExpirationCondition(arg0 database.NumberOperation, arg1 time.Time) database.Condition {
|
||||
m.ctrl.T.Helper()
|
||||
ret := m.ctrl.Call(m, "ExpirationCondition", op, expiration)
|
||||
ret := m.ctrl.Call(m, "ExpirationCondition", arg0, arg1)
|
||||
ret0, _ := ret[0].(database.Condition)
|
||||
return ret0
|
||||
}
|
||||
|
||||
// ExpirationCondition indicates an expected call of ExpirationCondition.
|
||||
func (mr *MockSessionRepositoryMockRecorder) ExpirationCondition(op, expiration any) *MockSessionRepositoryExpirationConditionCall {
|
||||
func (mr *MockSessionRepositoryMockRecorder) ExpirationCondition(arg0, arg1 any) *MockSessionRepositoryExpirationConditionCall {
|
||||
mr.mock.ctrl.T.Helper()
|
||||
call := mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ExpirationCondition", reflect.TypeOf((*MockSessionRepository)(nil).ExpirationCondition), op, expiration)
|
||||
call := mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ExpirationCondition", reflect.TypeOf((*MockSessionRepository)(nil).ExpirationCondition), arg0, arg1)
|
||||
return &MockSessionRepositoryExpirationConditionCall{Call: call}
|
||||
}
|
||||
|
||||
@@ -463,10 +463,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...)
|
||||
@@ -476,9 +476,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}
|
||||
}
|
||||
@@ -545,17 +545,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}
|
||||
}
|
||||
|
||||
@@ -621,17 +621,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}
|
||||
}
|
||||
|
||||
@@ -697,10 +697,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...)
|
||||
@@ -710,9 +710,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}
|
||||
}
|
||||
@@ -817,17 +817,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}
|
||||
}
|
||||
|
||||
@@ -855,17 +855,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}
|
||||
}
|
||||
|
||||
@@ -893,17 +893,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}
|
||||
}
|
||||
|
||||
@@ -931,17 +931,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}
|
||||
}
|
||||
|
||||
@@ -969,17 +969,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}
|
||||
}
|
||||
|
||||
@@ -1007,17 +1007,17 @@ func (c *MockSessionRepositorySetMetadataCall) DoAndReturn(f func([]*domain.Sess
|
||||
}
|
||||
|
||||
// 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}
|
||||
}
|
||||
|
||||
@@ -1045,17 +1045,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}
|
||||
}
|
||||
|
||||
@@ -1120,11 +1120,49 @@ func (c *MockSessionRepositoryTokenIDColumnCall) DoAndReturn(f func() database.C
|
||||
return c
|
||||
}
|
||||
|
||||
// Update mocks base method.
|
||||
func (m *MockSessionRepository) Update(ctx context.Context, client database.QueryExecutor, condition database.Condition, changes ...database.Change) (int64, error) {
|
||||
// TokenIDCondition mocks base method.
|
||||
func (m *MockSessionRepository) TokenIDCondition(arg0 string) database.Condition {
|
||||
m.ctrl.T.Helper()
|
||||
varargs := []any{ctx, client, condition}
|
||||
for _, a := range changes {
|
||||
ret := m.ctrl.Call(m, "TokenIDCondition", arg0)
|
||||
ret0, _ := ret[0].(database.Condition)
|
||||
return ret0
|
||||
}
|
||||
|
||||
// TokenIDCondition indicates an expected call of TokenIDCondition.
|
||||
func (mr *MockSessionRepositoryMockRecorder) TokenIDCondition(arg0 any) *MockSessionRepositoryTokenIDConditionCall {
|
||||
mr.mock.ctrl.T.Helper()
|
||||
call := mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "TokenIDCondition", reflect.TypeOf((*MockSessionRepository)(nil).TokenIDCondition), arg0)
|
||||
return &MockSessionRepositoryTokenIDConditionCall{Call: call}
|
||||
}
|
||||
|
||||
// MockSessionRepositoryTokenIDConditionCall wrap *gomock.Call
|
||||
type MockSessionRepositoryTokenIDConditionCall struct {
|
||||
*gomock.Call
|
||||
}
|
||||
|
||||
// Return rewrite *gomock.Call.Return
|
||||
func (c *MockSessionRepositoryTokenIDConditionCall) Return(arg0 database.Condition) *MockSessionRepositoryTokenIDConditionCall {
|
||||
c.Call = c.Call.Return(arg0)
|
||||
return c
|
||||
}
|
||||
|
||||
// Do rewrite *gomock.Call.Do
|
||||
func (c *MockSessionRepositoryTokenIDConditionCall) Do(f func(string) database.Condition) *MockSessionRepositoryTokenIDConditionCall {
|
||||
c.Call = c.Call.Do(f)
|
||||
return c
|
||||
}
|
||||
|
||||
// DoAndReturn rewrite *gomock.Call.DoAndReturn
|
||||
func (c *MockSessionRepositoryTokenIDConditionCall) DoAndReturn(f func(string) database.Condition) *MockSessionRepositoryTokenIDConditionCall {
|
||||
c.Call = c.Call.DoAndReturn(f)
|
||||
return c
|
||||
}
|
||||
|
||||
// Update mocks base method.
|
||||
func (m *MockSessionRepository) Update(arg0 context.Context, arg1 database.QueryExecutor, arg2 database.Condition, arg3 ...database.Change) (int64, error) {
|
||||
m.ctrl.T.Helper()
|
||||
varargs := []any{arg0, arg1, arg2}
|
||||
for _, a := range arg3 {
|
||||
varargs = append(varargs, a)
|
||||
}
|
||||
ret := m.ctrl.Call(m, "Update", varargs...)
|
||||
@@ -1134,9 +1172,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}
|
||||
}
|
||||
@@ -1203,17 +1241,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}
|
||||
}
|
||||
|
||||
@@ -1241,17 +1279,17 @@ func (c *MockSessionRepositoryUpdatedAtConditionCall) DoAndReturn(f func(databas
|
||||
}
|
||||
|
||||
// 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}
|
||||
}
|
||||
|
||||
@@ -1317,17 +1355,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}
|
||||
}
|
||||
|
||||
|
||||
@@ -2,6 +2,7 @@ package domainmock
|
||||
|
||||
import (
|
||||
context "context"
|
||||
"time"
|
||||
|
||||
gomock "go.uber.org/mock/gomock"
|
||||
|
||||
@@ -42,6 +43,6 @@ func (s *SessionRepo) Update(ctx context.Context, client database.QueryExecutor,
|
||||
return s.mock.Update(ctx, client, condition, changes...)
|
||||
}
|
||||
|
||||
func (s *SessionRepo) Delete(ctx context.Context, client database.QueryExecutor, condition database.Condition) (int64, error) {
|
||||
return s.mock.Delete(ctx, client, condition)
|
||||
func (s *SessionRepo) Delete(ctx context.Context, client database.QueryExecutor, condition database.Condition, permissionCondition database.Condition) (int64, time.Time, error) {
|
||||
return s.mock.Delete(ctx, client, condition, permissionCondition)
|
||||
}
|
||||
|
||||
@@ -11,6 +11,7 @@ const (
|
||||
OrganizationWritePermission = "organization.write"
|
||||
SessionReadPermission = "session.read"
|
||||
SessionWritePermission = "session.write"
|
||||
SessionDeletePermission = "session.delete"
|
||||
)
|
||||
|
||||
// PermissionRepository is the interface that manages and checks Zitadel permissions.
|
||||
|
||||
@@ -42,7 +42,14 @@ 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) (int64, error)
|
||||
// An additional permission condition can be provided to check if the session can be deleted, for example by checking the token id or user id.
|
||||
// The primary condition is used to select the session(s) to be deleted, while the permission condition can be used to verify that the session(s) meet certain criteria before deletion
|
||||
// respectively to return an error if the criteria are not met. In case no session matches the primary condition and a permission condition is provided,
|
||||
// the method will return a permission denied error, as the session(s) that should be deleted do not meet the criteria defined in the permission condition.
|
||||
// The method returns the number of deleted rows / session(s) and the latest deletion timestamp of the deleted session in case of a single deleted session.
|
||||
// In case the session was previously deleted, the method returns 0 and the previous deletion timestamp.
|
||||
// If multiple sessions are deleted, the method returns the number of deleted sessions and no timestamp, as it cannot be determined which session's deletion timestamp to return.
|
||||
Delete(ctx context.Context, client database.QueryExecutor, condition database.Condition, permissionCondition database.Condition) (int64, time.Time, error)
|
||||
}
|
||||
|
||||
// sessionColumns define all the columns of the session table.
|
||||
@@ -77,6 +84,8 @@ type sessionConditions interface {
|
||||
UserAgentIDCondition(userAgentID string) database.Condition
|
||||
// UserIDCondition returns an equal filter on the user id field.
|
||||
UserIDCondition(userID string) database.Condition
|
||||
// TokenIDCondition returns an equal filter on the token id field.
|
||||
TokenIDCondition(tokenID 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.
|
||||
|
||||
@@ -0,0 +1,127 @@
|
||||
package domain
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/zitadel/zitadel/backend/v3/storage/database"
|
||||
"github.com/zitadel/zitadel/internal/api/authz"
|
||||
"github.com/zitadel/zitadel/internal/eventstore"
|
||||
"github.com/zitadel/zitadel/internal/repository/session"
|
||||
"github.com/zitadel/zitadel/internal/zerrors"
|
||||
)
|
||||
|
||||
type DeleteSessionCommand struct {
|
||||
ID string `json:"id"`
|
||||
Token string `json:"token"`
|
||||
MustCheckPermission bool `json:"mustCheckPermission,omitempty"`
|
||||
|
||||
// DeletedAt is set after successful execution.
|
||||
DeletedAt time.Time `json:"deletedAt,omitempty"`
|
||||
// deletedRows is set after successful execution, used to determine if the session was deleted or not.
|
||||
deletedRows int64
|
||||
}
|
||||
|
||||
func NewDeleteSessionCommand(
|
||||
id string,
|
||||
token string,
|
||||
mustCheckPermission bool,
|
||||
) *DeleteSessionCommand {
|
||||
return &DeleteSessionCommand{
|
||||
ID: id,
|
||||
Token: token,
|
||||
MustCheckPermission: mustCheckPermission,
|
||||
}
|
||||
}
|
||||
|
||||
// RequiresTransaction implements [Transactional].
|
||||
func (cmd *DeleteSessionCommand) RequiresTransaction() {}
|
||||
|
||||
// Events implements [Commander].
|
||||
func (cmd *DeleteSessionCommand) Events(ctx context.Context, opts *InvokeOpts) ([]eventstore.Command, error) {
|
||||
if cmd.deletedRows == 0 {
|
||||
return nil, nil
|
||||
}
|
||||
return []eventstore.Command{
|
||||
session.NewTerminateEvent(ctx, &session.NewAggregate(cmd.ID, authz.GetInstance(ctx).InstanceID()).Aggregate),
|
||||
}, nil
|
||||
}
|
||||
|
||||
const (
|
||||
ErrSessionTokenInvalid = "ErrSessionTokenInvalid"
|
||||
ErrMoreThanOneRowAffected = "ErrMoreThanOneRowAffected"
|
||||
ErrIDMissing = "ErrIDMissing"
|
||||
ErrMissingPermission = "ErrMissingPermission"
|
||||
ErrInternal = "ErrInternal"
|
||||
)
|
||||
|
||||
func (cmd *DeleteSessionCommand) sessionDeletePermissionCheckCondition(ctx context.Context, sessionRepo SessionRepository, decryptor SessionTokenDecryptor) (database.Condition, error) {
|
||||
if !cmd.MustCheckPermission {
|
||||
return nil, nil
|
||||
}
|
||||
if cmd.Token != "" {
|
||||
sessionID, tokenID, err := decryptor(ctx, cmd.Token)
|
||||
if err != nil || sessionID != cmd.ID {
|
||||
return nil, zerrors.ThrowPermissionDenied(err, ErrSessionTokenInvalid, "Errors.Session.Token.Invalid")
|
||||
}
|
||||
return database.Or(
|
||||
sessionRepo.TokenIDCondition(tokenID),
|
||||
database.PermissionCheck(SessionDeletePermission, true),
|
||||
), nil
|
||||
}
|
||||
return database.Or(
|
||||
sessionRepo.UserIDCondition(authz.GetCtxData(ctx).UserID),
|
||||
database.PermissionCheck(SessionDeletePermission, true), // TODO: implement check once permissions are implemented
|
||||
), nil
|
||||
}
|
||||
|
||||
// Execute implements [Commander].
|
||||
func (cmd *DeleteSessionCommand) Execute(ctx context.Context, opts *InvokeOpts) (err error) {
|
||||
sessionRepo := opts.sessionRepo
|
||||
instance := authz.GetInstance(ctx)
|
||||
|
||||
permCheck, err := cmd.sessionDeletePermissionCheckCondition(ctx, sessionRepo, opts.sessionTokenDecryptor)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
deletedRows, deletedAt, err := sessionRepo.Delete(ctx, opts.DB(),
|
||||
sessionRepo.PrimaryKeyCondition(instance.InstanceID(), cmd.ID),
|
||||
permCheck,
|
||||
)
|
||||
if err != nil {
|
||||
if errors.Is(err, new(database.PermissionError)) {
|
||||
return zerrors.ThrowPermissionDenied(err, ErrMissingPermission,
|
||||
"insufficient permissions to delete session, require `session.delete` permission, ownership of the session or current session token")
|
||||
}
|
||||
return zerrors.ThrowInternal(err, ErrInternal, "an unexpected error occurred while deleting the session")
|
||||
}
|
||||
|
||||
if deletedRows > 1 {
|
||||
return zerrors.ThrowInternalf(nil, ErrMoreThanOneRowAffected, "expected 1 session to be deleted, got %d", deletedRows)
|
||||
}
|
||||
|
||||
cmd.DeletedAt = deletedAt
|
||||
cmd.deletedRows = deletedRows
|
||||
return nil
|
||||
}
|
||||
|
||||
// String implements [Commander].
|
||||
func (*DeleteSessionCommand) String() string {
|
||||
return "DeleteSessionCommand"
|
||||
}
|
||||
|
||||
// Validate implements [Commander].
|
||||
func (cmd *DeleteSessionCommand) Validate(ctx context.Context, opts *InvokeOpts) (err error) {
|
||||
if cmd.ID = strings.TrimSpace(cmd.ID); cmd.ID == "" {
|
||||
return zerrors.ThrowInvalidArgument(nil, ErrIDMissing, "Errors.IDMissing")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
var (
|
||||
_ Commander = (*DeleteSessionCommand)(nil)
|
||||
_ Transactional = (*DeleteSessionCommand)(nil)
|
||||
)
|
||||
@@ -0,0 +1,347 @@
|
||||
package domain_test
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/brianvoe/gofakeit/v6"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"go.uber.org/mock/gomock"
|
||||
|
||||
"github.com/zitadel/zitadel/backend/v3/domain"
|
||||
domainmock "github.com/zitadel/zitadel/backend/v3/domain/mock"
|
||||
"github.com/zitadel/zitadel/backend/v3/storage/database"
|
||||
noopdb "github.com/zitadel/zitadel/backend/v3/storage/database/dialect/noop"
|
||||
"github.com/zitadel/zitadel/internal/api/authz"
|
||||
zdomain "github.com/zitadel/zitadel/internal/domain"
|
||||
"github.com/zitadel/zitadel/internal/zerrors"
|
||||
)
|
||||
|
||||
func TestDeleteSessionCommand_Validate(t *testing.T) {
|
||||
t.Parallel()
|
||||
ctx := authz.NewMockContext("inst-1", "org-1", gofakeit.UUID())
|
||||
|
||||
type args struct {
|
||||
sessionID string
|
||||
sessionToken string
|
||||
mustCheckToken bool
|
||||
}
|
||||
type fields struct {
|
||||
sessionRepo func(ctrl *gomock.Controller) domain.SessionRepository
|
||||
permissionCheck func(ctrl *gomock.Controller) domain.PermissionChecker
|
||||
}
|
||||
tt := []struct {
|
||||
name string
|
||||
args args
|
||||
fields fields
|
||||
expectedError error
|
||||
}{
|
||||
{
|
||||
name: "when sessionID is empty should return invalid argument error",
|
||||
args: args{
|
||||
sessionID: "",
|
||||
},
|
||||
fields: fields{},
|
||||
expectedError: zerrors.ThrowInvalidArgument(nil, domain.ErrIDMissing, "Errors.IDMissing"),
|
||||
},
|
||||
{
|
||||
name: "when sessionID is provided must validate successfully",
|
||||
args: args{
|
||||
sessionID: "session",
|
||||
},
|
||||
fields: fields{},
|
||||
expectedError: nil,
|
||||
},
|
||||
}
|
||||
for _, tc := range tt {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
t.Parallel()
|
||||
// Given
|
||||
d := domain.NewDeleteSessionCommand(tc.args.sessionID, tc.args.sessionToken, tc.args.mustCheckToken)
|
||||
ctrl := gomock.NewController(t)
|
||||
opts := domain.DefaultOpts(nil)
|
||||
domain.WithQueryExecutor(new(noopdb.Pool))(opts)
|
||||
|
||||
if tc.fields.sessionRepo != nil {
|
||||
domain.WithSessionRepo(tc.fields.sessionRepo(ctrl))(opts)
|
||||
}
|
||||
if tc.fields.permissionCheck != nil {
|
||||
domain.WithPermissionChecker(tc.fields.permissionCheck(ctrl))(opts)
|
||||
}
|
||||
|
||||
// Test
|
||||
err := d.Validate(ctx, opts)
|
||||
|
||||
// Verify
|
||||
assert.Equal(t, tc.expectedError, err)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestDeleteSessionCommand_Execute(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
userID := gofakeit.UUID()
|
||||
ctx := authz.NewMockContext("inst-1", "org-1", userID)
|
||||
deleteErr := errors.New("delete error")
|
||||
|
||||
type args struct {
|
||||
mockTx func(ctrl *gomock.Controller) database.QueryExecutor
|
||||
sessionRepo func(ctrl *gomock.Controller) domain.SessionRepository
|
||||
sessionTokenDecryptor func(t *testing.T) domain.SessionTokenDecryptor
|
||||
}
|
||||
type fields struct {
|
||||
sessionID string
|
||||
sessionToken string
|
||||
mustCheckPermission bool
|
||||
}
|
||||
type res struct {
|
||||
error error
|
||||
deletedAt bool
|
||||
events int
|
||||
}
|
||||
tt := []struct {
|
||||
testName string
|
||||
|
||||
args args
|
||||
fields fields
|
||||
res res
|
||||
}{
|
||||
{
|
||||
testName: "when delete session fails should return error",
|
||||
args: args{
|
||||
sessionRepo: func(ctrl *gomock.Controller) domain.SessionRepository {
|
||||
repo := domainmock.NewSessionRepo(ctrl)
|
||||
repo.EXPECT().
|
||||
Delete(gomock.Any(), gomock.Any(),
|
||||
repo.PrimaryKeyCondition("inst-1", "session-1"),
|
||||
nil,
|
||||
).
|
||||
Times(1).
|
||||
Return(int64(0), time.Time{}, deleteErr)
|
||||
return repo
|
||||
},
|
||||
},
|
||||
fields: fields{
|
||||
sessionID: "session-1",
|
||||
},
|
||||
res: res{
|
||||
error: deleteErr,
|
||||
},
|
||||
},
|
||||
{
|
||||
testName: "when no token is provided but must check permission, should use userID check",
|
||||
args: args{
|
||||
sessionRepo: func(ctrl *gomock.Controller) domain.SessionRepository {
|
||||
repo := domainmock.NewSessionRepo(ctrl)
|
||||
repo.EXPECT().
|
||||
Delete(gomock.Any(), gomock.Any(),
|
||||
repo.PrimaryKeyCondition("inst-1", "session-1"),
|
||||
database.Or(
|
||||
repo.UserIDCondition(userID),
|
||||
database.PermissionCheck(zdomain.PermissionSessionDelete, true),
|
||||
),
|
||||
).
|
||||
Times(1).
|
||||
Return(int64(0), time.Time{}, nil)
|
||||
return repo
|
||||
},
|
||||
},
|
||||
fields: fields{
|
||||
sessionID: "session-1",
|
||||
mustCheckPermission: true,
|
||||
},
|
||||
res: res{
|
||||
error: nil,
|
||||
},
|
||||
},
|
||||
{
|
||||
testName: "when token is provided and must check permission, should use tokenID check",
|
||||
args: args{
|
||||
sessionRepo: func(ctrl *gomock.Controller) domain.SessionRepository {
|
||||
repo := domainmock.NewSessionRepo(ctrl)
|
||||
repo.EXPECT().
|
||||
Delete(gomock.Any(), gomock.Any(),
|
||||
repo.PrimaryKeyCondition("inst-1", "session-1"),
|
||||
database.Or(
|
||||
repo.TokenIDCondition("token-1"),
|
||||
database.PermissionCheck(zdomain.PermissionSessionDelete, true),
|
||||
),
|
||||
).
|
||||
Times(1).
|
||||
Return(int64(0), time.Time{}, nil)
|
||||
return repo
|
||||
},
|
||||
sessionTokenDecryptor: mockSessionTokenDecryptor("session-token-1", "session-1", "token-1"),
|
||||
},
|
||||
fields: fields{
|
||||
sessionID: "session-1",
|
||||
sessionToken: "session-token-1",
|
||||
mustCheckPermission: true,
|
||||
},
|
||||
res: res{
|
||||
error: nil,
|
||||
},
|
||||
},
|
||||
{
|
||||
testName: "when delete session without permission should return error",
|
||||
args: args{
|
||||
sessionRepo: func(ctrl *gomock.Controller) domain.SessionRepository {
|
||||
repo := domainmock.NewSessionRepo(ctrl)
|
||||
repo.EXPECT().
|
||||
Delete(gomock.Any(), gomock.Any(),
|
||||
repo.PrimaryKeyCondition("inst-1", "session-1"),
|
||||
database.Or(
|
||||
repo.UserIDCondition(userID),
|
||||
database.PermissionCheck(zdomain.PermissionSessionDelete, true),
|
||||
),
|
||||
).
|
||||
Times(1).
|
||||
Return(int64(0), time.Time{}, database.NewPermissionError(nil))
|
||||
return repo
|
||||
},
|
||||
},
|
||||
fields: fields{
|
||||
sessionID: "session-1",
|
||||
mustCheckPermission: true,
|
||||
},
|
||||
res: res{
|
||||
error: new(database.PermissionError),
|
||||
},
|
||||
},
|
||||
{
|
||||
testName: "when more than one row deleted must return internal error",
|
||||
args: args{
|
||||
sessionRepo: func(ctrl *gomock.Controller) domain.SessionRepository {
|
||||
repo := domainmock.NewSessionRepo(ctrl)
|
||||
repo.EXPECT().
|
||||
Delete(gomock.Any(), gomock.Any(),
|
||||
repo.PrimaryKeyCondition("inst-1", "session-1"),
|
||||
nil,
|
||||
).
|
||||
Times(1).
|
||||
Return(int64(2), time.Time{}, nil)
|
||||
return repo
|
||||
},
|
||||
},
|
||||
fields: fields{
|
||||
sessionID: "session-1",
|
||||
},
|
||||
res: res{
|
||||
error: zerrors.ThrowInternalf(nil, domain.ErrMoreThanOneRowAffected, "expected 1 session to be deleted, got %d", 2),
|
||||
},
|
||||
},
|
||||
{
|
||||
testName: "when no rows deleted should execute successfully",
|
||||
args: args{
|
||||
sessionRepo: func(ctrl *gomock.Controller) domain.SessionRepository {
|
||||
repo := domainmock.NewSessionRepo(ctrl)
|
||||
repo.EXPECT().
|
||||
Delete(gomock.Any(), gomock.Any(),
|
||||
repo.PrimaryKeyCondition("inst-1", "session-1"),
|
||||
nil,
|
||||
).
|
||||
Times(1).
|
||||
Return(int64(0), time.Time{}, nil)
|
||||
return repo
|
||||
},
|
||||
},
|
||||
fields: fields{
|
||||
sessionID: "session-1",
|
||||
},
|
||||
res: res{
|
||||
error: nil,
|
||||
},
|
||||
},
|
||||
{
|
||||
testName: "when one row deleted should execute successfully and set DeletedAt",
|
||||
args: args{
|
||||
sessionRepo: func(ctrl *gomock.Controller) domain.SessionRepository {
|
||||
repo := domainmock.NewSessionRepo(ctrl)
|
||||
repo.EXPECT().
|
||||
Delete(gomock.Any(), gomock.Any(),
|
||||
repo.PrimaryKeyCondition("inst-1", "session-1"),
|
||||
nil,
|
||||
).
|
||||
Times(1).
|
||||
Return(int64(1), time.Now(), nil)
|
||||
return repo
|
||||
},
|
||||
},
|
||||
fields: fields{
|
||||
sessionID: "session-1",
|
||||
},
|
||||
res: res{
|
||||
deletedAt: true,
|
||||
events: 1,
|
||||
},
|
||||
},
|
||||
{
|
||||
testName: "when session already deleted should execute successfully, set DeletedAt but no event",
|
||||
args: args{
|
||||
sessionRepo: func(ctrl *gomock.Controller) domain.SessionRepository {
|
||||
repo := domainmock.NewSessionRepo(ctrl)
|
||||
repo.EXPECT().
|
||||
Delete(gomock.Any(), gomock.Any(),
|
||||
repo.PrimaryKeyCondition("inst-1", "session-1"),
|
||||
nil,
|
||||
).
|
||||
Times(1).
|
||||
Return(int64(0), time.Now(), nil)
|
||||
return repo
|
||||
},
|
||||
},
|
||||
fields: fields{
|
||||
sessionID: "session-1",
|
||||
},
|
||||
res: res{
|
||||
deletedAt: true,
|
||||
events: 0,
|
||||
},
|
||||
},
|
||||
}
|
||||
for _, tc := range tt {
|
||||
t.Run(tc.testName, func(t *testing.T) {
|
||||
t.Parallel()
|
||||
// Given
|
||||
cmd := domain.NewDeleteSessionCommand(tc.fields.sessionID, tc.fields.sessionToken, tc.fields.mustCheckPermission)
|
||||
ctrl := gomock.NewController(t)
|
||||
opts := domain.DefaultOpts(nil)
|
||||
domain.WithQueryExecutor(new(noopdb.Pool))(opts)
|
||||
if tc.args.mockTx != nil {
|
||||
domain.WithQueryExecutor(tc.args.mockTx(ctrl))(opts)
|
||||
}
|
||||
if tc.args.sessionRepo != nil {
|
||||
domain.WithSessionRepo(tc.args.sessionRepo(ctrl))(opts)
|
||||
}
|
||||
if tc.args.sessionTokenDecryptor != nil {
|
||||
domain.WithSessionTokenDecryptor(tc.args.sessionTokenDecryptor(t))(opts)
|
||||
}
|
||||
|
||||
// Test
|
||||
err := cmd.Execute(ctx, opts)
|
||||
|
||||
// Verify
|
||||
assert.ErrorIs(t, err, tc.res.error)
|
||||
assert.Equal(t, tc.res.deletedAt, !cmd.DeletedAt.IsZero())
|
||||
|
||||
// Test events
|
||||
cmds, err := cmd.Events(ctx, opts)
|
||||
assert.NoError(t, err)
|
||||
assert.Len(t, cmds, tc.res.events)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func mockSessionTokenDecryptor(expectedSessionToken, returnSessionID, returnSessionToken string) func(t *testing.T) domain.SessionTokenDecryptor {
|
||||
return func(t *testing.T) domain.SessionTokenDecryptor {
|
||||
return func(ctx context.Context, sessionToken string) (sessionID, tokenID string, err error) {
|
||||
if sessionToken != expectedSessionToken {
|
||||
return "", "", zerrors.ThrowInvalidArgumentf(nil, "SESS-S3gq1", "Errors.Session.TokenInvalid")
|
||||
}
|
||||
return returnSessionID, returnSessionToken, nil
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
package domain
|
||||
|
||||
import "context"
|
||||
|
||||
type SessionTokenVerifier func(ctx context.Context, sessionToken, sessionID, tokenID string) (err error)
|
||||
|
||||
type SessionTokenDecryptor func(ctx context.Context, sessionToken string) (sessionID, tokenID string, err error)
|
||||
@@ -313,8 +313,14 @@ type existsCondition struct {
|
||||
|
||||
// Matches implements [Condition].
|
||||
func (e *existsCondition) Matches(x any) bool {
|
||||
// Unimplemented
|
||||
return false
|
||||
toMatch, ok := x.(*existsCondition)
|
||||
if !ok {
|
||||
return false
|
||||
}
|
||||
if e.table != toMatch.table {
|
||||
return false
|
||||
}
|
||||
return e.condition.Matches(toMatch.condition)
|
||||
}
|
||||
|
||||
// String implements [Condition].
|
||||
@@ -364,3 +370,46 @@ func ForceRestrictingColumn(cond Condition, isRestriction bool) Condition {
|
||||
}
|
||||
|
||||
var _ Condition = (*forcedRestrictingColumnCondition)(nil)
|
||||
|
||||
type permissionCheck struct {
|
||||
requiredPermission string
|
||||
throwError bool
|
||||
}
|
||||
|
||||
func (p *permissionCheck) Matches(x any) bool {
|
||||
toMatch, ok := x.(*permissionCheck)
|
||||
if !ok {
|
||||
return false
|
||||
}
|
||||
if toMatch.requiredPermission != p.requiredPermission {
|
||||
return false
|
||||
}
|
||||
return toMatch.throwError == p.throwError
|
||||
}
|
||||
|
||||
func (p *permissionCheck) String() string {
|
||||
return "database.permissionCheck"
|
||||
}
|
||||
|
||||
func (p *permissionCheck) Write(builder *StatementBuilder) {
|
||||
// TODO: implement actual permission check once we have the necessary tables and functions in place.
|
||||
// For now, we just write a dummy condition that can be used to verify that the permission check is included in the generated SQL.
|
||||
if p.throwError {
|
||||
builder.WriteString(" zitadel.throw_not_permitted() ")
|
||||
return
|
||||
}
|
||||
builder.WriteString(" true ")
|
||||
}
|
||||
|
||||
func (p *permissionCheck) IsRestrictingColumn(_ Column) bool {
|
||||
return false
|
||||
}
|
||||
|
||||
func PermissionCheck(requiredPermission string, throwError bool) *permissionCheck {
|
||||
return &permissionCheck{
|
||||
requiredPermission: requiredPermission,
|
||||
throwError: throwError,
|
||||
}
|
||||
}
|
||||
|
||||
var _ Condition = (*permissionCheck)(nil)
|
||||
|
||||
@@ -10,6 +10,10 @@ import (
|
||||
"github.com/zitadel/zitadel/backend/v3/storage/database"
|
||||
)
|
||||
|
||||
const (
|
||||
ZIT01 = "ZIT01" // custom error code to express missing permissions by the end-user
|
||||
)
|
||||
|
||||
func wrapError(err error) error {
|
||||
if err == nil {
|
||||
return nil
|
||||
@@ -54,6 +58,9 @@ func wrapPgError(err *pgconn.PgError) error {
|
||||
// 22P02: integrity violation - A value is being inserted into a enum that is not valid
|
||||
case "22P02":
|
||||
return database.NewCheckError(err.TableName, err.ConstraintName, err)
|
||||
// ZIT01: missing permissions by the end-user
|
||||
case ZIT01:
|
||||
return database.NewPermissionError(err)
|
||||
default:
|
||||
return database.NewUnknownError(err)
|
||||
}
|
||||
|
||||
@@ -0,0 +1,16 @@
|
||||
package migration
|
||||
|
||||
import (
|
||||
_ "embed"
|
||||
)
|
||||
|
||||
var (
|
||||
//go:embed 017_archived_sessions_table/up.sql
|
||||
up017ArchivedSessionsTable string
|
||||
//go:embed 017_archived_sessions_table/down.sql
|
||||
down017ArchivedSessionsTable string
|
||||
)
|
||||
|
||||
func init() {
|
||||
registerSQLMigration(17, up017ArchivedSessionsTable, down017ArchivedSessionsTable)
|
||||
}
|
||||
+4
@@ -0,0 +1,4 @@
|
||||
DROP TRIGGER IF EXISTS trg_move_to_archived_sessions ON zitadel.sessions;
|
||||
DROP FUNCTION IF EXISTS zitadel.move_to_archived_sessions() CASCADE;
|
||||
DROP TABLE IF EXISTS zitadel.archived_sessions CASCADE;
|
||||
DROP FUNCTION IF EXISTS zitadel.throw_not_permitted();
|
||||
+37
@@ -0,0 +1,37 @@
|
||||
CREATE TABLE zitadel.archived_sessions (
|
||||
instance_id TEXT NOT NULL
|
||||
, id TEXT NOT NULL CHECK (id <> '')
|
||||
, token_id TEXT
|
||||
, user_agent_id TEXT
|
||||
, expiration TIMESTAMPTZ
|
||||
, user_id TEXT
|
||||
, creator_id TEXT
|
||||
, deleted_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.move_to_archived_sessions() RETURNS trigger AS $$
|
||||
BEGIN
|
||||
INSERT INTO zitadel.archived_sessions (instance_id, id, token_id, user_agent_id, expiration, user_id, creator_id, deleted_at)
|
||||
VALUES (OLD.instance_id, OLD.id, OLD.token_id, OLD.user_agent_id, OLD.expiration, OLD.user_id, OLD.creator_id, now());
|
||||
RETURN OLD;
|
||||
END;
|
||||
$$ LANGUAGE plpgsql;
|
||||
|
||||
CREATE TRIGGER trg_move_to_archived_sessions
|
||||
AFTER DELETE ON zitadel.sessions
|
||||
FOR EACH ROW
|
||||
EXECUTE FUNCTION zitadel.move_to_archived_sessions();
|
||||
|
||||
-- placeholder for permissions, will be replaced by actual permission checks in the future,
|
||||
-- respectively will be used to fallback to when permissions are not yet implemented for a specific action
|
||||
CREATE OR REPLACE FUNCTION zitadel.throw_not_permitted() returns boolean AS $$
|
||||
BEGIN
|
||||
RAISE EXCEPTION 'Permission denied: User does not have permission'
|
||||
USING ERRCODE = 'ZIT01';
|
||||
END;
|
||||
$$ LANGUAGE plpgsql;
|
||||
@@ -272,3 +272,26 @@ func (e *UnknownError) Is(target error) bool {
|
||||
func (e *UnknownError) Unwrap() error {
|
||||
return e.original
|
||||
}
|
||||
|
||||
type PermissionError struct {
|
||||
original error
|
||||
}
|
||||
|
||||
func NewPermissionError(original error) error {
|
||||
return &PermissionError{
|
||||
original: original,
|
||||
}
|
||||
}
|
||||
|
||||
func (e *PermissionError) Error() string {
|
||||
return fmt.Sprintf("permission error: %v", e.original)
|
||||
}
|
||||
|
||||
func (e *PermissionError) Is(target error) bool {
|
||||
_, ok := target.(*PermissionError)
|
||||
return ok
|
||||
}
|
||||
|
||||
func (e *PermissionError) Unwrap() error {
|
||||
return e.original
|
||||
}
|
||||
|
||||
@@ -0,0 +1,6 @@
|
||||
package database
|
||||
|
||||
import "database/sql"
|
||||
|
||||
// Null is a type alias for sql.Null[T] to avoid importing the sql package into repositories.
|
||||
type Null[T any] = sql.Null[T]
|
||||
@@ -2,6 +2,7 @@ package repository
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"time"
|
||||
|
||||
@@ -228,14 +229,40 @@ func sessionCTE(change database.Change, i, j int, builder *database.StatementBui
|
||||
}
|
||||
|
||||
// Delete implements [domain.SessionRepository].
|
||||
func (s session) Delete(ctx context.Context, client database.QueryExecutor, condition database.Condition) (int64, error) {
|
||||
func (s session) Delete(ctx context.Context, client database.QueryExecutor, condition database.Condition, permissionCondition database.Condition) (int64, time.Time, error) {
|
||||
if !condition.IsRestrictingColumn(s.InstanceIDColumn()) {
|
||||
return 0, database.NewMissingConditionError(s.InstanceIDColumn())
|
||||
return 0, time.Time{}, database.NewMissingConditionError(s.InstanceIDColumn())
|
||||
}
|
||||
var builder database.StatementBuilder
|
||||
builder.WriteString("DELETE FROM zitadel.sessions")
|
||||
builder.WriteString("WITH gatekeeper AS (SELECT count(*) as total_found, MAX(sessions.deleted_at) as deleted_at FROM (SELECT instance_id, user_id, token_id, now() as deleted_at FROM zitadel.sessions")
|
||||
writeCondition(&builder, condition)
|
||||
return client.Exec(ctx, builder.String(), builder.Args()...)
|
||||
// In case the deletion is intended for a specific session,
|
||||
// we'll also include already deleted session to check the permission against,
|
||||
// and to return the deleted_at timestamp from the previous deletion in case the session was already deleted.
|
||||
if condition.IsRestrictingColumn(s.IDColumn()) {
|
||||
builder.WriteString(" UNION ALL SELECT instance_id, user_id, token_id, deleted_at FROM zitadel.archived_sessions as sessions")
|
||||
writeCondition(&builder, condition)
|
||||
}
|
||||
// join the users table to get the user's organization, which will be needed for checking the permission
|
||||
builder.WriteString(") as sessions")
|
||||
if permissionCondition != nil {
|
||||
builder.WriteString(" LEFT JOIN zitadel.users ON users.instance_id = sessions.instance_id AND users.id = sessions.user_id HAVING (bool_and(")
|
||||
permissionCondition.Write(&builder)
|
||||
builder.WriteString(") AND count(*) > 0) or zitadel.throw_not_permitted()") //TODO: change throw_not_permitted to actual permission check
|
||||
}
|
||||
builder.WriteString("), execution AS (DELETE FROM zitadel.sessions")
|
||||
writeCondition(&builder, condition)
|
||||
builder.WriteString(" and exists (SELECT 1 FROM gatekeeper) RETURNING 1 AS rows_affected) SELECT (CASE WHEN gatekeeper.total_found = 1 THEN gatekeeper.deleted_at END) as deleted_at, (SELECT count(*) FROM execution) as rows_affected FROM gatekeeper")
|
||||
var deletedAt database.Null[time.Time]
|
||||
var deletedSessions int64
|
||||
if err := client.QueryRow(ctx, builder.String(), builder.Args()...).Scan(&deletedAt, &deletedSessions); err != nil {
|
||||
// If the criteria did not match any session, ignore the error since the user would have had the permission to delete the session,
|
||||
if errors.Is(err, new(database.NoRowFoundError)) {
|
||||
return 0, time.Time{}, nil
|
||||
}
|
||||
return 0, time.Time{}, err
|
||||
}
|
||||
return deletedSessions, deletedAt.V, nil
|
||||
}
|
||||
|
||||
// -------------------------------------------------------------
|
||||
@@ -427,6 +454,11 @@ func (s session) UserIDCondition(userID string) database.Condition {
|
||||
return database.NewTextCondition(s.UserIDColumn(), database.TextOperationEqual, userID)
|
||||
}
|
||||
|
||||
// TokenIDCondition implements [domain.sessionConditions].
|
||||
func (s session) TokenIDCondition(tokenID string) database.Condition {
|
||||
return database.NewTextCondition(s.TokenIDColumn(), database.TextOperationEqual, tokenID)
|
||||
}
|
||||
|
||||
// CreatorIDCondition implements [domain.sessionConditions].
|
||||
func (s session) CreatorIDCondition(creatorID string) database.Condition {
|
||||
return database.NewTextCondition(s.CreatorIDColumn(), database.TextOperationEqual, creatorID)
|
||||
|
||||
@@ -918,6 +918,7 @@ func TestSession_Update(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestSession_Delete(t *testing.T) {
|
||||
start := time.Now()
|
||||
tx, err := pool.Begin(t.Context(), nil)
|
||||
require.NoError(t, err)
|
||||
defer func() {
|
||||
@@ -944,59 +945,207 @@ func TestSession_Delete(t *testing.T) {
|
||||
err = instanceRepo.Create(t.Context(), tx, &instance)
|
||||
require.NoError(t, err)
|
||||
|
||||
type test struct {
|
||||
name string
|
||||
testFunc func(t *testing.T) *domain.Session
|
||||
type fields struct {
|
||||
testFunc func(t *testing.T) []*domain.Session
|
||||
}
|
||||
type args struct {
|
||||
condition func([]*domain.Session) database.Condition
|
||||
permissionCondition func([]*domain.Session) database.Condition
|
||||
}
|
||||
type res struct {
|
||||
noOfDeletedRows int64
|
||||
deletedAt time.Time
|
||||
err error
|
||||
}
|
||||
|
||||
type test struct {
|
||||
name string
|
||||
fields fields
|
||||
args args
|
||||
res res
|
||||
}
|
||||
tests := []test{
|
||||
{
|
||||
name: "delete existent session",
|
||||
testFunc: func(t *testing.T) *domain.Session {
|
||||
session := &domain.Session{
|
||||
InstanceID: instanceId,
|
||||
ID: gofakeit.Name(),
|
||||
Lifetime: time.Hour * 24,
|
||||
CreatorID: gofakeit.Name(),
|
||||
}
|
||||
err := sessionRepo.Create(t.Context(), tx, session)
|
||||
require.NoError(t, err)
|
||||
return session
|
||||
fields: fields{
|
||||
testFunc: func(t *testing.T) []*domain.Session {
|
||||
session := &domain.Session{
|
||||
InstanceID: instanceId,
|
||||
ID: gofakeit.Name(),
|
||||
Lifetime: time.Hour * 24,
|
||||
CreatorID: gofakeit.Name(),
|
||||
}
|
||||
err := sessionRepo.Create(t.Context(), tx, session)
|
||||
require.NoError(t, err)
|
||||
return []*domain.Session{session}
|
||||
},
|
||||
},
|
||||
args: args{
|
||||
condition: func(sessions []*domain.Session) database.Condition {
|
||||
return sessionRepo.PrimaryKeyCondition(instanceId, sessions[0].ID)
|
||||
},
|
||||
},
|
||||
res: res{
|
||||
noOfDeletedRows: 1,
|
||||
deletedAt: time.Now(),
|
||||
},
|
||||
noOfDeletedRows: 1,
|
||||
},
|
||||
{
|
||||
name: "delete non existent session",
|
||||
testFunc: func(t *testing.T) *domain.Session {
|
||||
return &domain.Session{
|
||||
InstanceID: instanceId,
|
||||
ID: gofakeit.Name(),
|
||||
Lifetime: time.Hour * 24,
|
||||
CreatorID: gofakeit.Name(),
|
||||
}
|
||||
fields: fields{
|
||||
testFunc: func(t *testing.T) []*domain.Session {
|
||||
return []*domain.Session{}
|
||||
},
|
||||
},
|
||||
args: args{
|
||||
condition: func(sessions []*domain.Session) database.Condition {
|
||||
return sessionRepo.PrimaryKeyCondition(instanceId, gofakeit.UUID())
|
||||
},
|
||||
},
|
||||
res: res{
|
||||
noOfDeletedRows: 0,
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "delete previously existent session",
|
||||
fields: fields{
|
||||
testFunc: func(t *testing.T) []*domain.Session {
|
||||
session := &domain.Session{
|
||||
InstanceID: instanceId,
|
||||
ID: gofakeit.Name(),
|
||||
Lifetime: time.Hour * 24,
|
||||
CreatorID: gofakeit.Name(),
|
||||
}
|
||||
err := sessionRepo.Create(t.Context(), tx, session)
|
||||
require.NoError(t, err)
|
||||
_, _, err = sessionRepo.Delete(t.Context(), tx, sessionRepo.PrimaryKeyCondition(session.InstanceID, session.ID), nil)
|
||||
require.NoError(t, err)
|
||||
return []*domain.Session{session}
|
||||
},
|
||||
},
|
||||
args: args{
|
||||
condition: func(sessions []*domain.Session) database.Condition {
|
||||
return sessionRepo.PrimaryKeyCondition(instanceId, sessions[0].ID)
|
||||
},
|
||||
},
|
||||
res: res{
|
||||
noOfDeletedRows: 0,
|
||||
deletedAt: time.Now(),
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "delete multiple sessions",
|
||||
fields: fields{
|
||||
testFunc: func(t *testing.T) []*domain.Session {
|
||||
session := &domain.Session{
|
||||
InstanceID: instanceId,
|
||||
ID: gofakeit.Name(),
|
||||
Lifetime: time.Hour * 24,
|
||||
CreatorID: gofakeit.Name(),
|
||||
}
|
||||
err := sessionRepo.Create(t.Context(), tx, session)
|
||||
require.NoError(t, err)
|
||||
session2 := &domain.Session{
|
||||
InstanceID: instanceId,
|
||||
ID: gofakeit.Name(),
|
||||
Lifetime: time.Hour * 24,
|
||||
CreatorID: gofakeit.Name(),
|
||||
}
|
||||
err = sessionRepo.Create(t.Context(), tx, session2)
|
||||
require.NoError(t, err)
|
||||
return []*domain.Session{session, session2}
|
||||
},
|
||||
},
|
||||
args: args{
|
||||
condition: func(sessions []*domain.Session) database.Condition {
|
||||
return sessionRepo.InstanceIDCondition(instanceId)
|
||||
},
|
||||
},
|
||||
res: res{
|
||||
noOfDeletedRows: 2,
|
||||
deletedAt: time.Time{},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "delete without permission",
|
||||
fields: fields{
|
||||
testFunc: func(t *testing.T) []*domain.Session {
|
||||
session := &domain.Session{
|
||||
InstanceID: instanceId,
|
||||
ID: gofakeit.Name(),
|
||||
TokenID: gofakeit.UUID(),
|
||||
Lifetime: time.Hour * 24,
|
||||
CreatorID: gofakeit.Name(),
|
||||
}
|
||||
err := sessionRepo.Create(t.Context(), tx, session)
|
||||
require.NoError(t, err)
|
||||
return []*domain.Session{session}
|
||||
},
|
||||
},
|
||||
args: args{
|
||||
condition: func(sessions []*domain.Session) database.Condition {
|
||||
return sessionRepo.InstanceIDCondition(instanceId)
|
||||
},
|
||||
permissionCondition: func(sessions []*domain.Session) database.Condition {
|
||||
return sessionRepo.TokenIDCondition("invalid")
|
||||
},
|
||||
},
|
||||
res: res{
|
||||
noOfDeletedRows: 0,
|
||||
deletedAt: time.Time{},
|
||||
err: database.NewPermissionError(nil),
|
||||
},
|
||||
noOfDeletedRows: 0,
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
session := tt.testFunc(t)
|
||||
var sessions []*domain.Session
|
||||
if tt.fields.testFunc != nil {
|
||||
sessions = tt.fields.testFunc(t)
|
||||
}
|
||||
|
||||
var condition, permissionCondition database.Condition
|
||||
if tt.args.condition != nil {
|
||||
condition = tt.args.condition(sessions)
|
||||
}
|
||||
if tt.args.permissionCondition != nil {
|
||||
permissionCondition = tt.args.permissionCondition(sessions)
|
||||
}
|
||||
|
||||
// let's create a snapshot before deletion to make sure we could rollback in case of error
|
||||
snapshot, err := tx.Begin(t.Context())
|
||||
require.NoError(t, err)
|
||||
|
||||
// delete session
|
||||
deletedRows, err := sessionRepo.Delete(t.Context(), tx,
|
||||
sessionRepo.PrimaryKeyCondition(session.InstanceID, session.ID),
|
||||
deletedRows, deletedAt, err := sessionRepo.Delete(t.Context(), tx,
|
||||
condition, permissionCondition,
|
||||
)
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, tt.noOfDeletedRows, deletedRows)
|
||||
require.ErrorIs(t, err, tt.res.err)
|
||||
assert.Equal(t, tt.res.noOfDeletedRows, deletedRows)
|
||||
if tt.res.deletedAt.IsZero() {
|
||||
assert.Zero(t, deletedAt)
|
||||
} else {
|
||||
assert.WithinRange(t, deletedAt, start, time.Now())
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
err = snapshot.Rollback(t.Context())
|
||||
require.NoError(t, err)
|
||||
}
|
||||
|
||||
// verify session deletion
|
||||
deletedSession, err := sessionRepo.Get(t.Context(), tx,
|
||||
database.WithCondition(
|
||||
sessionRepo.PrimaryKeyCondition(session.InstanceID, session.ID),
|
||||
),
|
||||
deletedSessions, err := sessionRepo.List(t.Context(), tx,
|
||||
database.WithCondition(condition),
|
||||
)
|
||||
require.Error(t, err, new(database.NoRowFoundError))
|
||||
assert.Nil(t, deletedSession)
|
||||
require.NoError(t, err)
|
||||
expectedSessions := 0
|
||||
|
||||
// in case of permission error, sessions are not deleted,
|
||||
// so expectedSessions should be equal to the number of created sessions
|
||||
if tt.res.err != nil {
|
||||
expectedSessions = len(sessions)
|
||||
}
|
||||
assert.Len(t, deletedSessions, expectedSessions)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -212,6 +212,8 @@ func startZitadel(ctx context.Context, config *Config, masterKey string, server
|
||||
new_domain.SetIDPEncryptionAlgorithm(keys.IDPConfig)
|
||||
|
||||
sessionTokenVerifier := internal_authz.SessionTokenVerifier(keys.OIDC)
|
||||
sessionTokenDecryptor := internal_authz.SessionTokenDecryptor(keys.OIDC)
|
||||
new_domain.SetSessionTokenDecryptor(sessionTokenDecryptor)
|
||||
cacheConnectors, err := connector.StartConnectors(config.Caches, dbClient)
|
||||
if err != nil {
|
||||
return fmt.Errorf("unable to start caches: %w", err)
|
||||
|
||||
@@ -4,6 +4,7 @@ import (
|
||||
"context"
|
||||
"encoding/base64"
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
"github.com/zitadel/zitadel/internal/crypto"
|
||||
"github.com/zitadel/zitadel/internal/telemetry/tracing"
|
||||
@@ -30,3 +31,24 @@ func SessionTokenVerifier(algorithm crypto.EncryptionAlgorithm) func(ctx context
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
func SessionTokenDecryptor(algorithm crypto.EncryptionAlgorithm) func(ctx context.Context, sessionToken string) (sessionID, tokenID string, err error) {
|
||||
return func(ctx context.Context, sessionToken string) (sessionID, tokenID string, err error) {
|
||||
decodedToken, err := base64.RawURLEncoding.DecodeString(sessionToken)
|
||||
if err != nil {
|
||||
return "", "", zerrors.ThrowInvalidArgument(err, "AUTHZ-hi6Ph", "Errors.Session.Token.Invalid")
|
||||
}
|
||||
_, spanPasswordComparison := tracing.NewNamedSpan(ctx, "crypto.CompareHash")
|
||||
token, err := algorithm.DecryptString(decodedToken, algorithm.EncryptionKeyID())
|
||||
spanPasswordComparison.EndWithError(err)
|
||||
if err != nil {
|
||||
return "", "", zerrors.ThrowPermissionDenied(err, "AUTHZ-sGr42", "Errors.Session.Token.Invalid")
|
||||
}
|
||||
|
||||
n, err := fmt.Sscanf(strings.ReplaceAll(token, ":", " "), strings.ReplaceAll(SessionTokenFormat, ":", " "), &sessionID, &tokenID)
|
||||
if err != nil || n != 2 {
|
||||
return "", "", zerrors.ThrowInvalidArgument(err, "AUTHZ-3uike", "Errors.Session.Token.Invalid")
|
||||
}
|
||||
return sessionID, tokenID, nil
|
||||
}
|
||||
}
|
||||
|
||||
@@ -21,6 +21,7 @@ import (
|
||||
|
||||
"github.com/zitadel/zitadel/internal/integration"
|
||||
"github.com/zitadel/zitadel/internal/integration/sink"
|
||||
"github.com/zitadel/zitadel/pkg/grpc/feature/v2"
|
||||
mgmt "github.com/zitadel/zitadel/pkg/grpc/management"
|
||||
"github.com/zitadel/zitadel/pkg/grpc/object/v2"
|
||||
"github.com/zitadel/zitadel/pkg/grpc/session/v2"
|
||||
@@ -897,95 +898,150 @@ func TestServer_SetSession_expired(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestServer_DeleteSession_token(t *testing.T) {
|
||||
createResp, err := Client.CreateSession(LoginCTX, &session.CreateSessionRequest{})
|
||||
require.NoError(t, err)
|
||||
sysAuthZ := integration.WithSystemAuthorization(CTX)
|
||||
|
||||
_, err = Client.DeleteSession(CTX, &session.DeleteSessionRequest{
|
||||
SessionId: createResp.GetSessionId(),
|
||||
SessionToken: gu.Ptr("invalid"),
|
||||
})
|
||||
require.Error(t, err)
|
||||
relTableState := integration.RelationalTablesEnableMatrix(t, IAMOwnerCTX, sysAuthZ)
|
||||
for _, stateCase := range relTableState {
|
||||
t.Run(stateCase.Name, func(t *testing.T) {
|
||||
loginCTX := stateCase.Inst.WithAuthorizationToken(t.Context(), integration.UserTypeLogin)
|
||||
createResp, err := stateCase.Inst.Client.SessionV2.CreateSession(loginCTX, &session.CreateSessionRequest{})
|
||||
require.NoError(t, err)
|
||||
|
||||
_, err = Client.DeleteSession(CTX, &session.DeleteSessionRequest{
|
||||
SessionId: createResp.GetSessionId(),
|
||||
SessionToken: gu.Ptr(createResp.GetSessionToken()),
|
||||
})
|
||||
require.NoError(t, err)
|
||||
//TODO: remove after create session also uses the relational tables directly
|
||||
if stateCase.Enabled {
|
||||
time.Sleep(5 * time.Second)
|
||||
}
|
||||
|
||||
_, err = stateCase.Inst.Client.SessionV2.DeleteSession(stateCase.InstOwner, &session.DeleteSessionRequest{
|
||||
SessionId: createResp.GetSessionId(),
|
||||
SessionToken: gu.Ptr("invalid"),
|
||||
})
|
||||
require.Error(t, err)
|
||||
|
||||
_, err = stateCase.Inst.Client.SessionV2.DeleteSession(stateCase.InstOwner, &session.DeleteSessionRequest{
|
||||
SessionId: createResp.GetSessionId(),
|
||||
SessionToken: gu.Ptr(createResp.GetSessionToken()),
|
||||
})
|
||||
require.NoError(t, err)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestServer_DeleteSession_own_session(t *testing.T) {
|
||||
// create two users for the test and a session each to get tokens for authorization
|
||||
user1 := Instance.CreateHumanUser(CTX)
|
||||
Instance.SetUserPassword(CTX, user1.GetUserId(), integration.UserPassword, false)
|
||||
_, token1, _, _ := Instance.CreatePasswordSession(t, LoginCTX, user1.GetUserId(), integration.UserPassword)
|
||||
sysAuthZ := integration.WithSystemAuthorization(CTX)
|
||||
|
||||
user2 := Instance.CreateHumanUser(CTX)
|
||||
Instance.SetUserPassword(CTX, user2.GetUserId(), integration.UserPassword, false)
|
||||
_, token2, _, _ := Instance.CreatePasswordSession(t, LoginCTX, user2.GetUserId(), integration.UserPassword)
|
||||
relTableState := integration.RelationalTablesEnableMatrix(t, IAMOwnerCTX, sysAuthZ)
|
||||
for _, stateCase := range relTableState {
|
||||
t.Run(stateCase.Name, func(t *testing.T) {
|
||||
loginCTX := stateCase.Inst.WithAuthorizationToken(t.Context(), integration.UserTypeLogin)
|
||||
// create two users for the test and a session each to get tokens for authorization
|
||||
user1 := stateCase.Inst.CreateUserTypeHuman(stateCase.InstOwner, integration.Email())
|
||||
stateCase.Inst.SetUserPassword(stateCase.InstOwner, user1.GetId(), integration.UserPassword, false)
|
||||
_, token1, _, _ := stateCase.Inst.CreatePasswordSession(t, loginCTX, user1.GetId(), integration.UserPassword)
|
||||
|
||||
// create a new session for the first user
|
||||
createResp, err := Client.CreateSession(LoginCTX, &session.CreateSessionRequest{
|
||||
Checks: &session.Checks{
|
||||
User: &session.CheckUser{
|
||||
Search: &session.CheckUser_UserId{
|
||||
UserId: user1.GetUserId(),
|
||||
user2 := stateCase.Inst.CreateUserTypeHuman(stateCase.InstOwner, integration.Email())
|
||||
stateCase.Inst.SetUserPassword(stateCase.InstOwner, user2.GetId(), integration.UserPassword, false)
|
||||
_, token2, _, _ := stateCase.Inst.CreatePasswordSession(t, loginCTX, user2.GetId(), integration.UserPassword)
|
||||
|
||||
// create a new session for the first user
|
||||
createResp, err := stateCase.Inst.Client.SessionV2.CreateSession(loginCTX, &session.CreateSessionRequest{
|
||||
Checks: &session.Checks{
|
||||
User: &session.CheckUser{
|
||||
Search: &session.CheckUser_UserId{
|
||||
UserId: user1.GetId(),
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
})
|
||||
require.NoError(t, err)
|
||||
})
|
||||
require.NoError(t, err)
|
||||
|
||||
// delete the new (user1) session must not be possible with user (has no permission)
|
||||
_, err = Client.DeleteSession(integration.WithAuthorizationToken(context.Background(), token2), &session.DeleteSessionRequest{
|
||||
SessionId: createResp.GetSessionId(),
|
||||
})
|
||||
require.Error(t, err)
|
||||
//TODO: remove after create session also uses the relational tables directly
|
||||
if stateCase.Enabled {
|
||||
time.Sleep(5 * time.Second)
|
||||
}
|
||||
|
||||
// delete the new (user1) session by themselves
|
||||
_, err = Client.DeleteSession(integration.WithAuthorizationToken(context.Background(), token1), &session.DeleteSessionRequest{
|
||||
SessionId: createResp.GetSessionId(),
|
||||
})
|
||||
require.NoError(t, err)
|
||||
// delete the new (user1) session must not be possible with user (has no permission)
|
||||
_, err = stateCase.Inst.Client.SessionV2.DeleteSession(integration.WithAuthorizationToken(t.Context(), token2), &session.DeleteSessionRequest{
|
||||
SessionId: createResp.GetSessionId(),
|
||||
})
|
||||
require.Error(t, err)
|
||||
|
||||
// delete the new (user1) session by themselves
|
||||
_, err = stateCase.Inst.Client.SessionV2.DeleteSession(integration.WithAuthorizationToken(t.Context(), token1), &session.DeleteSessionRequest{
|
||||
SessionId: createResp.GetSessionId(),
|
||||
})
|
||||
require.NoError(t, err)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestServer_DeleteSession_with_permission(t *testing.T) {
|
||||
createResp, err := Client.CreateSession(LoginCTX, &session.CreateSessionRequest{
|
||||
Checks: &session.Checks{
|
||||
User: &session.CheckUser{
|
||||
Search: &session.CheckUser_UserId{
|
||||
UserId: User.GetUserId(),
|
||||
},
|
||||
},
|
||||
},
|
||||
})
|
||||
require.NoError(t, err)
|
||||
sysAuthZ := integration.WithSystemAuthorization(CTX)
|
||||
|
||||
// delete the new session by ORG_OWNER
|
||||
_, err = Client.DeleteSession(Instance.WithAuthorization(context.Background(), integration.UserTypeOrgOwner), &session.DeleteSessionRequest{
|
||||
SessionId: createResp.GetSessionId(),
|
||||
})
|
||||
require.NoError(t, err)
|
||||
relTableState := integration.RelationalTablesEnableMatrix(t, IAMOwnerCTX, sysAuthZ)
|
||||
|
||||
for _, stateCase := range relTableState {
|
||||
t.Run(stateCase.Name, func(t *testing.T) {
|
||||
loginCTX := stateCase.Inst.WithAuthorizationToken(t.Context(), integration.UserTypeLogin)
|
||||
user1 := stateCase.Inst.CreateUserTypeHuman(stateCase.InstOwner, integration.Email())
|
||||
createResp, err := stateCase.Inst.Client.SessionV2.CreateSession(loginCTX, &session.CreateSessionRequest{
|
||||
Checks: &session.Checks{
|
||||
User: &session.CheckUser{
|
||||
Search: &session.CheckUser_UserId{
|
||||
UserId: user1.GetId(),
|
||||
},
|
||||
},
|
||||
},
|
||||
})
|
||||
require.NoError(t, err)
|
||||
|
||||
if stateCase.Enabled {
|
||||
t.Skip("TODO: remove after permission checks are implemented in the relational tables")
|
||||
}
|
||||
// delete the new session by ORG_OWNER
|
||||
_, err = stateCase.Inst.Client.SessionV2.DeleteSession(
|
||||
stateCase.Inst.WithAuthorizationToken(t.Context(), integration.UserTypeOrgOwner),
|
||||
&session.DeleteSessionRequest{
|
||||
SessionId: createResp.GetSessionId(),
|
||||
},
|
||||
)
|
||||
require.NoError(t, err)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestServer_DeleteSession_expired(t *testing.T) {
|
||||
createResp, err := Client.CreateSession(LoginCTX, &session.CreateSessionRequest{
|
||||
Lifetime: durationpb.New(5 * time.Second),
|
||||
t.Cleanup(func() {
|
||||
_, err := Instance.Client.FeatureV2.ResetInstanceFeatures(IAMOwnerCTX, &feature.ResetInstanceFeaturesRequest{})
|
||||
require.NoError(t, err)
|
||||
})
|
||||
require.NoError(t, err)
|
||||
sysAuthZ := integration.WithSystemAuthorization(CTX)
|
||||
|
||||
// wait until the token expires
|
||||
time.Sleep(10 * time.Second)
|
||||
_, err = Client.DeleteSession(Instance.WithAuthorizationToken(context.Background(), integration.UserTypeOrgOwner), &session.DeleteSessionRequest{
|
||||
SessionId: createResp.GetSessionId(),
|
||||
SessionToken: gu.Ptr(createResp.GetSessionToken()),
|
||||
})
|
||||
require.NoError(t, err)
|
||||
relTableState := integration.RelationalTablesEnableMatrix(t, IAMOwnerCTX, sysAuthZ)
|
||||
|
||||
// get session should return an error
|
||||
sessionResp, err := Client.GetSession(Instance.WithAuthorizationToken(context.Background(), integration.UserTypeOrgOwner),
|
||||
&session.GetSessionRequest{SessionId: createResp.GetSessionId()})
|
||||
require.Error(t, err)
|
||||
require.Nil(t, sessionResp)
|
||||
for _, stateCase := range relTableState {
|
||||
t.Run(stateCase.Name, func(t *testing.T) {
|
||||
loginCTX := stateCase.Inst.WithAuthorizationToken(t.Context(), integration.UserTypeLogin)
|
||||
createResp, err := stateCase.Inst.Client.SessionV2.CreateSession(loginCTX, &session.CreateSessionRequest{
|
||||
Lifetime: durationpb.New(5 * time.Second),
|
||||
})
|
||||
require.NoError(t, err)
|
||||
|
||||
// wait until the token expires
|
||||
time.Sleep(10 * time.Second)
|
||||
_, err = stateCase.Inst.Client.SessionV2.DeleteSession(stateCase.Inst.WithAuthorizationToken(t.Context(), integration.UserTypeOrgOwner), &session.DeleteSessionRequest{
|
||||
SessionId: createResp.GetSessionId(),
|
||||
SessionToken: gu.Ptr(createResp.GetSessionToken()),
|
||||
})
|
||||
require.NoError(t, err)
|
||||
|
||||
// get session should return an error
|
||||
sessionResp, err := stateCase.Inst.Client.SessionV2.GetSession(stateCase.Inst.WithAuthorizationToken(t.Context(), integration.UserTypeOrgOwner),
|
||||
&session.GetSessionRequest{SessionId: createResp.GetSessionId()})
|
||||
require.Error(t, err)
|
||||
require.Nil(t, sessionResp)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func Test_ZITADEL_API_missing_authentication(t *testing.T) {
|
||||
|
||||
@@ -10,6 +10,8 @@ import (
|
||||
"golang.org/x/text/language"
|
||||
"google.golang.org/protobuf/types/known/structpb"
|
||||
|
||||
sessionv2 "github.com/zitadel/zitadel/backend/v3/api/session/v2"
|
||||
"github.com/zitadel/zitadel/internal/api/authz"
|
||||
"github.com/zitadel/zitadel/internal/api/grpc/object/v2"
|
||||
"github.com/zitadel/zitadel/internal/command"
|
||||
"github.com/zitadel/zitadel/internal/domain"
|
||||
@@ -63,6 +65,10 @@ func (s *Server) SetSession(ctx context.Context, req *connect.Request[session.Se
|
||||
}
|
||||
|
||||
func (s *Server) DeleteSession(ctx context.Context, req *connect.Request[session.DeleteSessionRequest]) (*connect.Response[session.DeleteSessionResponse], error) {
|
||||
if authz.GetFeatures(ctx).EnableRelationalTables {
|
||||
return sessionv2.DeleteSession(ctx, req)
|
||||
}
|
||||
|
||||
details, err := s.command.TerminateSession(ctx, req.Msg.GetSessionId(), req.Msg.GetSessionToken())
|
||||
if err != nil {
|
||||
return nil, err
|
||||
|
||||
@@ -16,6 +16,8 @@ import (
|
||||
"github.com/zitadel/oidc/v3/pkg/oidc"
|
||||
"github.com/zitadel/oidc/v3/pkg/op"
|
||||
|
||||
zdomain "github.com/zitadel/zitadel/backend/v3/domain"
|
||||
"github.com/zitadel/zitadel/backend/v3/storage/database/repository"
|
||||
"github.com/zitadel/zitadel/internal/api/authz"
|
||||
http_utils "github.com/zitadel/zitadel/internal/api/http"
|
||||
"github.com/zitadel/zitadel/internal/api/http/middleware"
|
||||
@@ -350,6 +352,16 @@ func (o *OPStorage) TerminateSessionFromRequest(ctx context.Context, endSessionR
|
||||
|
||||
// V2:
|
||||
// Terminate the v2 session of the id_token_hint
|
||||
if authz.GetFeatures(ctx).EnableRelationalTables {
|
||||
if err := zdomain.Invoke(
|
||||
ctx,
|
||||
zdomain.NewDeleteSessionCommand(endSessionRequest.IDTokenHintClaims.SessionID, "", false),
|
||||
zdomain.WithSessionRepo(repository.SessionRepository()),
|
||||
); err != nil {
|
||||
return "", err
|
||||
}
|
||||
return v2PostLogoutRedirectURI(endSessionRequest.RedirectURI), nil
|
||||
}
|
||||
_, err = o.command.TerminateSessionWithoutTokenCheck(ctx, endSessionRequest.IDTokenHintClaims.SessionID)
|
||||
if err != nil {
|
||||
return "", err
|
||||
|
||||
@@ -428,7 +428,7 @@ func (p *relationalTablesProjection) reduceSessionTerminated(event eventstore.Ev
|
||||
|
||||
sessionRepo := repository.SessionRepository()
|
||||
condition := sessionRepo.PrimaryKeyCondition(e.Aggregate().InstanceID, e.Aggregate().ID)
|
||||
_, err := sessionRepo.Delete(ctx, v3Tx, condition)
|
||||
_, _, err := sessionRepo.Delete(ctx, v3Tx, condition, nil)
|
||||
return err
|
||||
}), nil
|
||||
}
|
||||
|
||||
@@ -160,7 +160,7 @@ func (p *relationalTablesProjection) reduceUserLocked(event eventstore.Event) (*
|
||||
sessionRepo.InstanceIDCondition(event.Aggregate().InstanceID),
|
||||
sessionRepo.UserIDCondition(event.Aggregate().ID),
|
||||
)
|
||||
_, err := sessionRepo.Delete(ctx, v3Tx, condition)
|
||||
_, _, err := sessionRepo.Delete(ctx, v3Tx, condition, nil)
|
||||
return err
|
||||
}),
|
||||
), nil
|
||||
@@ -223,7 +223,7 @@ func (p *relationalTablesProjection) reduceUserDeactivated(event eventstore.Even
|
||||
sessionRepo.InstanceIDCondition(event.Aggregate().InstanceID),
|
||||
sessionRepo.UserIDCondition(event.Aggregate().ID),
|
||||
)
|
||||
_, err := sessionRepo.Delete(ctx, v3Tx, condition)
|
||||
_, _, err := sessionRepo.Delete(ctx, v3Tx, condition, nil)
|
||||
return err
|
||||
}),
|
||||
), nil
|
||||
|
||||
Reference in New Issue
Block a user