From 8472c7683d76f322e01f4dc69bc38e681383598d Mon Sep 17 00:00:00 2001 From: Gayathri Vijayan <66356931+grvijayan@users.noreply.github.com> Date: Fri, 19 Dec 2025 07:03:27 +0100 Subject: [PATCH] feat(rt): authorizations (#11066) # Which Problems Are Solved This PR implements relational table and repository interface for the authorizations feature as described in https://github.com/zitadel/zitadel/issues/10767 # How the Problems Are Solved By * adding new `zitadel.authorizations` and `zitadel.authorization_roles` tables * implementing the `AuthorizationRepository` interface to create, update, query, and delete user authorizations and roles * implementing `authorization_relational.go` projection to update `zitadel.authorizations` and `zitadel.authorization_roles` tables based on events # Additional Changes N/A # Additional Context - Closes #10767 --------- Co-authored-by: Livio Spring --- backend/v3/domain/authorization.go | 100 ++ .../v3/domain/authorizationstate_enumer.go | 109 ++ backend/v3/domain/mock/authorization.mock.go | 975 ++++++++++++++ .../migration/010_authorizations_table.go | 16 + .../010_authorizations_table/down.sql | 3 + .../migration/010_authorizations_table/up.sql | 40 + .../events_testing/authorization_test.go | 505 +++++++ .../database/events_testing/events_test.go | 24 +- .../database/repository/authorization.go | 319 +++++ .../database/repository/authorization_test.go | 1199 +++++++++++++++++ .../database/repository/repository_test.go | 18 + .../projection/authorization_relational.go | 188 +++ internal/query/projection/projection.go | 3 + 13 files changed, 3490 insertions(+), 9 deletions(-) create mode 100644 backend/v3/domain/authorization.go create mode 100644 backend/v3/domain/authorizationstate_enumer.go create mode 100644 backend/v3/domain/mock/authorization.mock.go create mode 100644 backend/v3/storage/database/dialect/postgres/migration/010_authorizations_table.go create mode 100644 backend/v3/storage/database/dialect/postgres/migration/010_authorizations_table/down.sql create mode 100644 backend/v3/storage/database/dialect/postgres/migration/010_authorizations_table/up.sql create mode 100644 backend/v3/storage/database/events_testing/authorization_test.go create mode 100644 backend/v3/storage/database/repository/authorization.go create mode 100644 backend/v3/storage/database/repository/authorization_test.go create mode 100644 internal/query/projection/authorization_relational.go diff --git a/backend/v3/domain/authorization.go b/backend/v3/domain/authorization.go new file mode 100644 index 000000000..5f098b5a6 --- /dev/null +++ b/backend/v3/domain/authorization.go @@ -0,0 +1,100 @@ +package domain + +import ( + "context" + "time" + + "github.com/zitadel/zitadel/backend/v3/storage/database" +) + +//go:generate enumer -type AuthorizationState -transform lower -trimprefix AuthorizationState -sql +type AuthorizationState uint8 + +const ( + AuthorizationStateActive AuthorizationState = iota + AuthorizationStateInactive +) + +type Authorization struct { + ID string `json:"id,omitempty" db:"id"` + UserID string `json:"userId" db:"user_id"` + ProjectID string `json:"projectId" db:"project_id"` + GrantID *string `json:"grantId" db:"grant_id"` + InstanceID string `json:"instanceId,omitempty" db:"instance_id"` + State AuthorizationState `json:"state,omitempty" db:"state"` + CreatedAt time.Time `json:"createdAt,omitempty" db:"created_at"` + UpdatedAt time.Time `json:"updatedAt,omitempty" db:"updated_at"` + Roles []string `json:"roles,omitempty" db:"roles"` +} + +// authorizationColumns defines all the columns of the authorizations table. +type authorizationColumns interface { + // PrimaryKeyColumns returns the columns for the primary key fields + PrimaryKeyColumns() []database.Column + // IDColumn returns the column for the id field. + IDColumn() database.Column + // UserIDColumn returns the column for the user_id field. + UserIDColumn() database.Column + // GrantIDColumn returns the column for the project grant_id field. + GrantIDColumn() database.Column + // ProjectIDColumn returns the column for the project_id field. + ProjectIDColumn() database.Column + // InstanceIDColumn returns the column for the instance_id field. + InstanceIDColumn() database.Column + // StateColumn returns the column for the state field. + StateColumn() database.Column + // CreatedAtColumn returns the column for the created_at field. + CreatedAtColumn() database.Column + // UpdatedAtColumn returns the column for the updated_at field. + UpdatedAtColumn() database.Column +} + +// authorizationConditions defines all the conditions for the authorizations table. +type authorizationConditions interface { + // PrimaryKeyCondition returns a filter on the primary key fields. + PrimaryKeyCondition(instanceID, id string) database.Condition + // IDCondition returns an equal filter on the id field. + IDCondition(id string) database.Condition + // InstanceIDCondition returns a filter on the instance_id field. + InstanceIDCondition(instanceID string) database.Condition + // ProjectIDCondition returns a filter on the project_id field. + ProjectIDCondition(projectID string) database.Condition + // GrantIDCondition returns a filter on the grant_id field. + GrantIDCondition(grantID string) database.Condition + // UserIDCondition returns a filter on the user_id field. + UserIDCondition(userID string) database.Condition + // RoleCondition returns a filter on the role_key field from the authorization_roles table. + RoleCondition(op database.TextOperation, role string) database.Condition + // ExistsRole returns a filter that checks for the existence of a role key in the authorization_roles table. + ExistsRole(cond database.Condition) database.Condition + // StateCondition returns a filter on the name field. + StateCondition(state AuthorizationState) database.Condition +} + +// authorizationChanges defines all the changes for the authorizations table. +type authorizationChanges interface { + // SetUpdatedAt sets the updated at column. + // Only use this when reducing events, + // during regular updates the DB sets this column automatically. + SetUpdatedAt(updatedAt time.Time) database.Change + // SetState sets the state column. + SetState(state AuthorizationState) database.Change +} + +//go:generate mockgen -typed -package domainmock -destination ./mock/authorization.mock.go . AuthorizationRepository + +// AuthorizationRepository is the interface for the authorization repository. +type AuthorizationRepository interface { + Repository + + authorizationConditions + authorizationChanges + authorizationColumns + + Get(ctx context.Context, client database.QueryExecutor, opts ...database.QueryOption) (*Authorization, error) + List(ctx context.Context, client database.QueryExecutor, opts ...database.QueryOption) ([]*Authorization, error) + + Create(ctx context.Context, client database.QueryExecutor, authorization *Authorization) error + Update(ctx context.Context, client database.QueryExecutor, condition database.Condition, roles []string, changes ...database.Change) (int64, error) + Delete(ctx context.Context, client database.QueryExecutor, condition database.Condition) (int64, error) +} diff --git a/backend/v3/domain/authorizationstate_enumer.go b/backend/v3/domain/authorizationstate_enumer.go new file mode 100644 index 000000000..65dc99b04 --- /dev/null +++ b/backend/v3/domain/authorizationstate_enumer.go @@ -0,0 +1,109 @@ +// Code generated by "enumer -type AuthorizationState -transform lower -trimprefix AuthorizationState -sql"; DO NOT EDIT. + +package domain + +import ( + "database/sql/driver" + "fmt" + "strings" +) + +const _AuthorizationStateName = "activeinactive" + +var _AuthorizationStateIndex = [...]uint8{0, 6, 14} + +const _AuthorizationStateLowerName = "activeinactive" + +func (i AuthorizationState) String() string { + if i >= AuthorizationState(len(_AuthorizationStateIndex)-1) { + return fmt.Sprintf("AuthorizationState(%d)", i) + } + return _AuthorizationStateName[_AuthorizationStateIndex[i]:_AuthorizationStateIndex[i+1]] +} + +// An "invalid array index" compiler error signifies that the constant values have changed. +// Re-run the stringer command to generate them again. +func _AuthorizationStateNoOp() { + var x [1]struct{} + _ = x[AuthorizationStateActive-(0)] + _ = x[AuthorizationStateInactive-(1)] +} + +var _AuthorizationStateValues = []AuthorizationState{AuthorizationStateActive, AuthorizationStateInactive} + +var _AuthorizationStateNameToValueMap = map[string]AuthorizationState{ + _AuthorizationStateName[0:6]: AuthorizationStateActive, + _AuthorizationStateLowerName[0:6]: AuthorizationStateActive, + _AuthorizationStateName[6:14]: AuthorizationStateInactive, + _AuthorizationStateLowerName[6:14]: AuthorizationStateInactive, +} + +var _AuthorizationStateNames = []string{ + _AuthorizationStateName[0:6], + _AuthorizationStateName[6:14], +} + +// AuthorizationStateString retrieves an enum value from the enum constants string name. +// Throws an error if the param is not part of the enum. +func AuthorizationStateString(s string) (AuthorizationState, error) { + if val, ok := _AuthorizationStateNameToValueMap[s]; ok { + return val, nil + } + + if val, ok := _AuthorizationStateNameToValueMap[strings.ToLower(s)]; ok { + return val, nil + } + return 0, fmt.Errorf("%s does not belong to AuthorizationState values", s) +} + +// AuthorizationStateValues returns all values of the enum +func AuthorizationStateValues() []AuthorizationState { + return _AuthorizationStateValues +} + +// AuthorizationStateStrings returns a slice of all String values of the enum +func AuthorizationStateStrings() []string { + strs := make([]string, len(_AuthorizationStateNames)) + copy(strs, _AuthorizationStateNames) + return strs +} + +// IsAAuthorizationState returns "true" if the value is listed in the enum definition. "false" otherwise +func (i AuthorizationState) IsAAuthorizationState() bool { + for _, v := range _AuthorizationStateValues { + if i == v { + return true + } + } + return false +} + +func (i AuthorizationState) Value() (driver.Value, error) { + return i.String(), nil +} + +func (i *AuthorizationState) Scan(value interface{}) error { + if value == nil { + return nil + } + + var str string + switch v := value.(type) { + case []byte: + str = string(v) + case string: + str = v + case fmt.Stringer: + str = v.String() + default: + return fmt.Errorf("invalid value of AuthorizationState: %[1]T(%[1]v)", value) + } + + val, err := AuthorizationStateString(str) + if err != nil { + return err + } + + *i = val + return nil +} diff --git a/backend/v3/domain/mock/authorization.mock.go b/backend/v3/domain/mock/authorization.mock.go new file mode 100644 index 000000000..d600fa9c8 --- /dev/null +++ b/backend/v3/domain/mock/authorization.mock.go @@ -0,0 +1,975 @@ +// Code generated by MockGen. DO NOT EDIT. +// Source: github.com/zitadel/zitadel/backend/v3/domain (interfaces: AuthorizationRepository) +// +// Generated by this command: +// +// mockgen -typed -package domainmock -destination ./mock/authorization.mock.go . AuthorizationRepository +// + +// Package domainmock is a generated GoMock package. +package domainmock + +import ( + context "context" + reflect "reflect" + time "time" + + domain "github.com/zitadel/zitadel/backend/v3/domain" + database "github.com/zitadel/zitadel/backend/v3/storage/database" + gomock "go.uber.org/mock/gomock" +) + +// MockAuthorizationRepository is a mock of AuthorizationRepository interface. +type MockAuthorizationRepository struct { + ctrl *gomock.Controller + recorder *MockAuthorizationRepositoryMockRecorder + isgomock struct{} +} + +// MockAuthorizationRepositoryMockRecorder is the mock recorder for MockAuthorizationRepository. +type MockAuthorizationRepositoryMockRecorder struct { + mock *MockAuthorizationRepository +} + +// NewMockAuthorizationRepository creates a new mock instance. +func NewMockAuthorizationRepository(ctrl *gomock.Controller) *MockAuthorizationRepository { + mock := &MockAuthorizationRepository{ctrl: ctrl} + mock.recorder = &MockAuthorizationRepositoryMockRecorder{mock} + return mock +} + +// EXPECT returns an object that allows the caller to indicate expected use. +func (m *MockAuthorizationRepository) EXPECT() *MockAuthorizationRepositoryMockRecorder { + return m.recorder +} + +// Create mocks base method. +func (m *MockAuthorizationRepository) Create(ctx context.Context, client database.QueryExecutor, authorization *domain.Authorization) error { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "Create", ctx, client, authorization) + ret0, _ := ret[0].(error) + return ret0 +} + +// Create indicates an expected call of Create. +func (mr *MockAuthorizationRepositoryMockRecorder) Create(ctx, client, authorization any) *MockAuthorizationRepositoryCreateCall { + mr.mock.ctrl.T.Helper() + call := mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Create", reflect.TypeOf((*MockAuthorizationRepository)(nil).Create), ctx, client, authorization) + return &MockAuthorizationRepositoryCreateCall{Call: call} +} + +// MockAuthorizationRepositoryCreateCall wrap *gomock.Call +type MockAuthorizationRepositoryCreateCall struct { + *gomock.Call +} + +// Return rewrite *gomock.Call.Return +func (c *MockAuthorizationRepositoryCreateCall) Return(arg0 error) *MockAuthorizationRepositoryCreateCall { + c.Call = c.Call.Return(arg0) + return c +} + +// Do rewrite *gomock.Call.Do +func (c *MockAuthorizationRepositoryCreateCall) Do(f func(context.Context, database.QueryExecutor, *domain.Authorization) error) *MockAuthorizationRepositoryCreateCall { + c.Call = c.Call.Do(f) + return c +} + +// DoAndReturn rewrite *gomock.Call.DoAndReturn +func (c *MockAuthorizationRepositoryCreateCall) DoAndReturn(f func(context.Context, database.QueryExecutor, *domain.Authorization) error) *MockAuthorizationRepositoryCreateCall { + c.Call = c.Call.DoAndReturn(f) + return c +} + +// CreatedAtColumn mocks base method. +func (m *MockAuthorizationRepository) CreatedAtColumn() database.Column { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "CreatedAtColumn") + ret0, _ := ret[0].(database.Column) + return ret0 +} + +// CreatedAtColumn indicates an expected call of CreatedAtColumn. +func (mr *MockAuthorizationRepositoryMockRecorder) CreatedAtColumn() *MockAuthorizationRepositoryCreatedAtColumnCall { + mr.mock.ctrl.T.Helper() + call := mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "CreatedAtColumn", reflect.TypeOf((*MockAuthorizationRepository)(nil).CreatedAtColumn)) + return &MockAuthorizationRepositoryCreatedAtColumnCall{Call: call} +} + +// MockAuthorizationRepositoryCreatedAtColumnCall wrap *gomock.Call +type MockAuthorizationRepositoryCreatedAtColumnCall struct { + *gomock.Call +} + +// Return rewrite *gomock.Call.Return +func (c *MockAuthorizationRepositoryCreatedAtColumnCall) Return(arg0 database.Column) *MockAuthorizationRepositoryCreatedAtColumnCall { + c.Call = c.Call.Return(arg0) + return c +} + +// Do rewrite *gomock.Call.Do +func (c *MockAuthorizationRepositoryCreatedAtColumnCall) Do(f func() database.Column) *MockAuthorizationRepositoryCreatedAtColumnCall { + c.Call = c.Call.Do(f) + return c +} + +// DoAndReturn rewrite *gomock.Call.DoAndReturn +func (c *MockAuthorizationRepositoryCreatedAtColumnCall) DoAndReturn(f func() database.Column) *MockAuthorizationRepositoryCreatedAtColumnCall { + c.Call = c.Call.DoAndReturn(f) + return c +} + +// Delete mocks base method. +func (m *MockAuthorizationRepository) Delete(ctx context.Context, client database.QueryExecutor, condition database.Condition) (int64, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "Delete", ctx, client, condition) + ret0, _ := ret[0].(int64) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// Delete indicates an expected call of Delete. +func (mr *MockAuthorizationRepositoryMockRecorder) Delete(ctx, client, condition any) *MockAuthorizationRepositoryDeleteCall { + mr.mock.ctrl.T.Helper() + call := mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Delete", reflect.TypeOf((*MockAuthorizationRepository)(nil).Delete), ctx, client, condition) + return &MockAuthorizationRepositoryDeleteCall{Call: call} +} + +// MockAuthorizationRepositoryDeleteCall wrap *gomock.Call +type MockAuthorizationRepositoryDeleteCall struct { + *gomock.Call +} + +// Return rewrite *gomock.Call.Return +func (c *MockAuthorizationRepositoryDeleteCall) Return(arg0 int64, arg1 error) *MockAuthorizationRepositoryDeleteCall { + c.Call = c.Call.Return(arg0, arg1) + return c +} + +// Do rewrite *gomock.Call.Do +func (c *MockAuthorizationRepositoryDeleteCall) Do(f func(context.Context, database.QueryExecutor, database.Condition) (int64, error)) *MockAuthorizationRepositoryDeleteCall { + c.Call = c.Call.Do(f) + return c +} + +// DoAndReturn rewrite *gomock.Call.DoAndReturn +func (c *MockAuthorizationRepositoryDeleteCall) DoAndReturn(f func(context.Context, database.QueryExecutor, database.Condition) (int64, error)) *MockAuthorizationRepositoryDeleteCall { + c.Call = c.Call.DoAndReturn(f) + return c +} + +// Get mocks base method. +func (m *MockAuthorizationRepository) Get(ctx context.Context, client database.QueryExecutor, opts ...database.QueryOption) (*domain.Authorization, error) { + m.ctrl.T.Helper() + varargs := []any{ctx, client} + for _, a := range opts { + varargs = append(varargs, a) + } + ret := m.ctrl.Call(m, "Get", varargs...) + ret0, _ := ret[0].(*domain.Authorization) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// Get indicates an expected call of Get. +func (mr *MockAuthorizationRepositoryMockRecorder) Get(ctx, client any, opts ...any) *MockAuthorizationRepositoryGetCall { + mr.mock.ctrl.T.Helper() + varargs := append([]any{ctx, client}, opts...) + call := mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Get", reflect.TypeOf((*MockAuthorizationRepository)(nil).Get), varargs...) + return &MockAuthorizationRepositoryGetCall{Call: call} +} + +// MockAuthorizationRepositoryGetCall wrap *gomock.Call +type MockAuthorizationRepositoryGetCall struct { + *gomock.Call +} + +// Return rewrite *gomock.Call.Return +func (c *MockAuthorizationRepositoryGetCall) Return(arg0 *domain.Authorization, arg1 error) *MockAuthorizationRepositoryGetCall { + c.Call = c.Call.Return(arg0, arg1) + return c +} + +// Do rewrite *gomock.Call.Do +func (c *MockAuthorizationRepositoryGetCall) Do(f func(context.Context, database.QueryExecutor, ...database.QueryOption) (*domain.Authorization, error)) *MockAuthorizationRepositoryGetCall { + c.Call = c.Call.Do(f) + return c +} + +// DoAndReturn rewrite *gomock.Call.DoAndReturn +func (c *MockAuthorizationRepositoryGetCall) DoAndReturn(f func(context.Context, database.QueryExecutor, ...database.QueryOption) (*domain.Authorization, error)) *MockAuthorizationRepositoryGetCall { + c.Call = c.Call.DoAndReturn(f) + return c +} + +// GrantIDColumn mocks base method. +func (m *MockAuthorizationRepository) GrantIDColumn() database.Column { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "GrantIDColumn") + ret0, _ := ret[0].(database.Column) + return ret0 +} + +// GrantIDColumn indicates an expected call of GrantIDColumn. +func (mr *MockAuthorizationRepositoryMockRecorder) GrantIDColumn() *MockAuthorizationRepositoryGrantIDColumnCall { + mr.mock.ctrl.T.Helper() + call := mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GrantIDColumn", reflect.TypeOf((*MockAuthorizationRepository)(nil).GrantIDColumn)) + return &MockAuthorizationRepositoryGrantIDColumnCall{Call: call} +} + +// MockAuthorizationRepositoryGrantIDColumnCall wrap *gomock.Call +type MockAuthorizationRepositoryGrantIDColumnCall struct { + *gomock.Call +} + +// Return rewrite *gomock.Call.Return +func (c *MockAuthorizationRepositoryGrantIDColumnCall) Return(arg0 database.Column) *MockAuthorizationRepositoryGrantIDColumnCall { + c.Call = c.Call.Return(arg0) + return c +} + +// Do rewrite *gomock.Call.Do +func (c *MockAuthorizationRepositoryGrantIDColumnCall) Do(f func() database.Column) *MockAuthorizationRepositoryGrantIDColumnCall { + c.Call = c.Call.Do(f) + return c +} + +// DoAndReturn rewrite *gomock.Call.DoAndReturn +func (c *MockAuthorizationRepositoryGrantIDColumnCall) DoAndReturn(f func() database.Column) *MockAuthorizationRepositoryGrantIDColumnCall { + c.Call = c.Call.DoAndReturn(f) + return c +} + +// GrantIDCondition mocks base method. +func (m *MockAuthorizationRepository) GrantIDCondition(grantID string) database.Condition { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "GrantIDCondition", grantID) + ret0, _ := ret[0].(database.Condition) + return ret0 +} + +// GrantIDCondition indicates an expected call of GrantIDCondition. +func (mr *MockAuthorizationRepositoryMockRecorder) GrantIDCondition(grantID any) *MockAuthorizationRepositoryGrantIDConditionCall { + mr.mock.ctrl.T.Helper() + call := mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GrantIDCondition", reflect.TypeOf((*MockAuthorizationRepository)(nil).GrantIDCondition), grantID) + return &MockAuthorizationRepositoryGrantIDConditionCall{Call: call} +} + +// MockAuthorizationRepositoryGrantIDConditionCall wrap *gomock.Call +type MockAuthorizationRepositoryGrantIDConditionCall struct { + *gomock.Call +} + +// Return rewrite *gomock.Call.Return +func (c *MockAuthorizationRepositoryGrantIDConditionCall) Return(arg0 database.Condition) *MockAuthorizationRepositoryGrantIDConditionCall { + c.Call = c.Call.Return(arg0) + return c +} + +// Do rewrite *gomock.Call.Do +func (c *MockAuthorizationRepositoryGrantIDConditionCall) Do(f func(string) database.Condition) *MockAuthorizationRepositoryGrantIDConditionCall { + c.Call = c.Call.Do(f) + return c +} + +// DoAndReturn rewrite *gomock.Call.DoAndReturn +func (c *MockAuthorizationRepositoryGrantIDConditionCall) DoAndReturn(f func(string) database.Condition) *MockAuthorizationRepositoryGrantIDConditionCall { + c.Call = c.Call.DoAndReturn(f) + return c +} + +// IDColumn mocks base method. +func (m *MockAuthorizationRepository) IDColumn() database.Column { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "IDColumn") + ret0, _ := ret[0].(database.Column) + return ret0 +} + +// IDColumn indicates an expected call of IDColumn. +func (mr *MockAuthorizationRepositoryMockRecorder) IDColumn() *MockAuthorizationRepositoryIDColumnCall { + mr.mock.ctrl.T.Helper() + call := mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "IDColumn", reflect.TypeOf((*MockAuthorizationRepository)(nil).IDColumn)) + return &MockAuthorizationRepositoryIDColumnCall{Call: call} +} + +// MockAuthorizationRepositoryIDColumnCall wrap *gomock.Call +type MockAuthorizationRepositoryIDColumnCall struct { + *gomock.Call +} + +// Return rewrite *gomock.Call.Return +func (c *MockAuthorizationRepositoryIDColumnCall) Return(arg0 database.Column) *MockAuthorizationRepositoryIDColumnCall { + c.Call = c.Call.Return(arg0) + return c +} + +// Do rewrite *gomock.Call.Do +func (c *MockAuthorizationRepositoryIDColumnCall) Do(f func() database.Column) *MockAuthorizationRepositoryIDColumnCall { + c.Call = c.Call.Do(f) + return c +} + +// DoAndReturn rewrite *gomock.Call.DoAndReturn +func (c *MockAuthorizationRepositoryIDColumnCall) DoAndReturn(f func() database.Column) *MockAuthorizationRepositoryIDColumnCall { + c.Call = c.Call.DoAndReturn(f) + return c +} + +// IDCondition mocks base method. +func (m *MockAuthorizationRepository) IDCondition(id string) database.Condition { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "IDCondition", id) + ret0, _ := ret[0].(database.Condition) + return ret0 +} + +// IDCondition indicates an expected call of IDCondition. +func (mr *MockAuthorizationRepositoryMockRecorder) IDCondition(id any) *MockAuthorizationRepositoryIDConditionCall { + mr.mock.ctrl.T.Helper() + call := mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "IDCondition", reflect.TypeOf((*MockAuthorizationRepository)(nil).IDCondition), id) + return &MockAuthorizationRepositoryIDConditionCall{Call: call} +} + +// MockAuthorizationRepositoryIDConditionCall wrap *gomock.Call +type MockAuthorizationRepositoryIDConditionCall struct { + *gomock.Call +} + +// Return rewrite *gomock.Call.Return +func (c *MockAuthorizationRepositoryIDConditionCall) Return(arg0 database.Condition) *MockAuthorizationRepositoryIDConditionCall { + c.Call = c.Call.Return(arg0) + return c +} + +// Do rewrite *gomock.Call.Do +func (c *MockAuthorizationRepositoryIDConditionCall) Do(f func(string) database.Condition) *MockAuthorizationRepositoryIDConditionCall { + c.Call = c.Call.Do(f) + return c +} + +// DoAndReturn rewrite *gomock.Call.DoAndReturn +func (c *MockAuthorizationRepositoryIDConditionCall) DoAndReturn(f func(string) database.Condition) *MockAuthorizationRepositoryIDConditionCall { + c.Call = c.Call.DoAndReturn(f) + return c +} + +// InstanceIDColumn mocks base method. +func (m *MockAuthorizationRepository) InstanceIDColumn() database.Column { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "InstanceIDColumn") + ret0, _ := ret[0].(database.Column) + return ret0 +} + +// InstanceIDColumn indicates an expected call of InstanceIDColumn. +func (mr *MockAuthorizationRepositoryMockRecorder) InstanceIDColumn() *MockAuthorizationRepositoryInstanceIDColumnCall { + mr.mock.ctrl.T.Helper() + call := mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "InstanceIDColumn", reflect.TypeOf((*MockAuthorizationRepository)(nil).InstanceIDColumn)) + return &MockAuthorizationRepositoryInstanceIDColumnCall{Call: call} +} + +// MockAuthorizationRepositoryInstanceIDColumnCall wrap *gomock.Call +type MockAuthorizationRepositoryInstanceIDColumnCall struct { + *gomock.Call +} + +// Return rewrite *gomock.Call.Return +func (c *MockAuthorizationRepositoryInstanceIDColumnCall) Return(arg0 database.Column) *MockAuthorizationRepositoryInstanceIDColumnCall { + c.Call = c.Call.Return(arg0) + return c +} + +// Do rewrite *gomock.Call.Do +func (c *MockAuthorizationRepositoryInstanceIDColumnCall) Do(f func() database.Column) *MockAuthorizationRepositoryInstanceIDColumnCall { + c.Call = c.Call.Do(f) + return c +} + +// DoAndReturn rewrite *gomock.Call.DoAndReturn +func (c *MockAuthorizationRepositoryInstanceIDColumnCall) DoAndReturn(f func() database.Column) *MockAuthorizationRepositoryInstanceIDColumnCall { + c.Call = c.Call.DoAndReturn(f) + return c +} + +// InstanceIDCondition mocks base method. +func (m *MockAuthorizationRepository) InstanceIDCondition(instanceID string) database.Condition { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "InstanceIDCondition", instanceID) + ret0, _ := ret[0].(database.Condition) + return ret0 +} + +// InstanceIDCondition indicates an expected call of InstanceIDCondition. +func (mr *MockAuthorizationRepositoryMockRecorder) InstanceIDCondition(instanceID any) *MockAuthorizationRepositoryInstanceIDConditionCall { + mr.mock.ctrl.T.Helper() + call := mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "InstanceIDCondition", reflect.TypeOf((*MockAuthorizationRepository)(nil).InstanceIDCondition), instanceID) + return &MockAuthorizationRepositoryInstanceIDConditionCall{Call: call} +} + +// MockAuthorizationRepositoryInstanceIDConditionCall wrap *gomock.Call +type MockAuthorizationRepositoryInstanceIDConditionCall struct { + *gomock.Call +} + +// Return rewrite *gomock.Call.Return +func (c *MockAuthorizationRepositoryInstanceIDConditionCall) Return(arg0 database.Condition) *MockAuthorizationRepositoryInstanceIDConditionCall { + c.Call = c.Call.Return(arg0) + return c +} + +// Do rewrite *gomock.Call.Do +func (c *MockAuthorizationRepositoryInstanceIDConditionCall) Do(f func(string) database.Condition) *MockAuthorizationRepositoryInstanceIDConditionCall { + c.Call = c.Call.Do(f) + return c +} + +// DoAndReturn rewrite *gomock.Call.DoAndReturn +func (c *MockAuthorizationRepositoryInstanceIDConditionCall) DoAndReturn(f func(string) database.Condition) *MockAuthorizationRepositoryInstanceIDConditionCall { + c.Call = c.Call.DoAndReturn(f) + return c +} + +// List mocks base method. +func (m *MockAuthorizationRepository) List(ctx context.Context, client database.QueryExecutor, opts ...database.QueryOption) ([]*domain.Authorization, error) { + m.ctrl.T.Helper() + varargs := []any{ctx, client} + for _, a := range opts { + varargs = append(varargs, a) + } + ret := m.ctrl.Call(m, "List", varargs...) + ret0, _ := ret[0].([]*domain.Authorization) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// List indicates an expected call of List. +func (mr *MockAuthorizationRepositoryMockRecorder) List(ctx, client any, opts ...any) *MockAuthorizationRepositoryListCall { + mr.mock.ctrl.T.Helper() + varargs := append([]any{ctx, client}, opts...) + call := mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "List", reflect.TypeOf((*MockAuthorizationRepository)(nil).List), varargs...) + return &MockAuthorizationRepositoryListCall{Call: call} +} + +// MockAuthorizationRepositoryListCall wrap *gomock.Call +type MockAuthorizationRepositoryListCall struct { + *gomock.Call +} + +// Return rewrite *gomock.Call.Return +func (c *MockAuthorizationRepositoryListCall) Return(arg0 []*domain.Authorization, arg1 error) *MockAuthorizationRepositoryListCall { + c.Call = c.Call.Return(arg0, arg1) + return c +} + +// Do rewrite *gomock.Call.Do +func (c *MockAuthorizationRepositoryListCall) Do(f func(context.Context, database.QueryExecutor, ...database.QueryOption) ([]*domain.Authorization, error)) *MockAuthorizationRepositoryListCall { + c.Call = c.Call.Do(f) + return c +} + +// DoAndReturn rewrite *gomock.Call.DoAndReturn +func (c *MockAuthorizationRepositoryListCall) DoAndReturn(f func(context.Context, database.QueryExecutor, ...database.QueryOption) ([]*domain.Authorization, error)) *MockAuthorizationRepositoryListCall { + c.Call = c.Call.DoAndReturn(f) + return c +} + +// PrimaryKeyColumns mocks base method. +func (m *MockAuthorizationRepository) PrimaryKeyColumns() []database.Column { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "PrimaryKeyColumns") + ret0, _ := ret[0].([]database.Column) + return ret0 +} + +// PrimaryKeyColumns indicates an expected call of PrimaryKeyColumns. +func (mr *MockAuthorizationRepositoryMockRecorder) PrimaryKeyColumns() *MockAuthorizationRepositoryPrimaryKeyColumnsCall { + mr.mock.ctrl.T.Helper() + call := mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "PrimaryKeyColumns", reflect.TypeOf((*MockAuthorizationRepository)(nil).PrimaryKeyColumns)) + return &MockAuthorizationRepositoryPrimaryKeyColumnsCall{Call: call} +} + +// MockAuthorizationRepositoryPrimaryKeyColumnsCall wrap *gomock.Call +type MockAuthorizationRepositoryPrimaryKeyColumnsCall struct { + *gomock.Call +} + +// Return rewrite *gomock.Call.Return +func (c *MockAuthorizationRepositoryPrimaryKeyColumnsCall) Return(arg0 []database.Column) *MockAuthorizationRepositoryPrimaryKeyColumnsCall { + c.Call = c.Call.Return(arg0) + return c +} + +// Do rewrite *gomock.Call.Do +func (c *MockAuthorizationRepositoryPrimaryKeyColumnsCall) Do(f func() []database.Column) *MockAuthorizationRepositoryPrimaryKeyColumnsCall { + c.Call = c.Call.Do(f) + return c +} + +// DoAndReturn rewrite *gomock.Call.DoAndReturn +func (c *MockAuthorizationRepositoryPrimaryKeyColumnsCall) DoAndReturn(f func() []database.Column) *MockAuthorizationRepositoryPrimaryKeyColumnsCall { + c.Call = c.Call.DoAndReturn(f) + return c +} + +// PrimaryKeyCondition mocks base method. +func (m *MockAuthorizationRepository) PrimaryKeyCondition(instanceID, id string) database.Condition { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "PrimaryKeyCondition", instanceID, id) + ret0, _ := ret[0].(database.Condition) + return ret0 +} + +// PrimaryKeyCondition indicates an expected call of PrimaryKeyCondition. +func (mr *MockAuthorizationRepositoryMockRecorder) PrimaryKeyCondition(instanceID, id any) *MockAuthorizationRepositoryPrimaryKeyConditionCall { + mr.mock.ctrl.T.Helper() + call := mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "PrimaryKeyCondition", reflect.TypeOf((*MockAuthorizationRepository)(nil).PrimaryKeyCondition), instanceID, id) + return &MockAuthorizationRepositoryPrimaryKeyConditionCall{Call: call} +} + +// MockAuthorizationRepositoryPrimaryKeyConditionCall wrap *gomock.Call +type MockAuthorizationRepositoryPrimaryKeyConditionCall struct { + *gomock.Call +} + +// Return rewrite *gomock.Call.Return +func (c *MockAuthorizationRepositoryPrimaryKeyConditionCall) Return(arg0 database.Condition) *MockAuthorizationRepositoryPrimaryKeyConditionCall { + c.Call = c.Call.Return(arg0) + return c +} + +// Do rewrite *gomock.Call.Do +func (c *MockAuthorizationRepositoryPrimaryKeyConditionCall) Do(f func(string, string) database.Condition) *MockAuthorizationRepositoryPrimaryKeyConditionCall { + c.Call = c.Call.Do(f) + return c +} + +// DoAndReturn rewrite *gomock.Call.DoAndReturn +func (c *MockAuthorizationRepositoryPrimaryKeyConditionCall) DoAndReturn(f func(string, string) database.Condition) *MockAuthorizationRepositoryPrimaryKeyConditionCall { + c.Call = c.Call.DoAndReturn(f) + return c +} + +// ProjectIDColumn mocks base method. +func (m *MockAuthorizationRepository) ProjectIDColumn() database.Column { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "ProjectIDColumn") + ret0, _ := ret[0].(database.Column) + return ret0 +} + +// ProjectIDColumn indicates an expected call of ProjectIDColumn. +func (mr *MockAuthorizationRepositoryMockRecorder) ProjectIDColumn() *MockAuthorizationRepositoryProjectIDColumnCall { + mr.mock.ctrl.T.Helper() + call := mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ProjectIDColumn", reflect.TypeOf((*MockAuthorizationRepository)(nil).ProjectIDColumn)) + return &MockAuthorizationRepositoryProjectIDColumnCall{Call: call} +} + +// MockAuthorizationRepositoryProjectIDColumnCall wrap *gomock.Call +type MockAuthorizationRepositoryProjectIDColumnCall struct { + *gomock.Call +} + +// Return rewrite *gomock.Call.Return +func (c *MockAuthorizationRepositoryProjectIDColumnCall) Return(arg0 database.Column) *MockAuthorizationRepositoryProjectIDColumnCall { + c.Call = c.Call.Return(arg0) + return c +} + +// Do rewrite *gomock.Call.Do +func (c *MockAuthorizationRepositoryProjectIDColumnCall) Do(f func() database.Column) *MockAuthorizationRepositoryProjectIDColumnCall { + c.Call = c.Call.Do(f) + return c +} + +// DoAndReturn rewrite *gomock.Call.DoAndReturn +func (c *MockAuthorizationRepositoryProjectIDColumnCall) DoAndReturn(f func() database.Column) *MockAuthorizationRepositoryProjectIDColumnCall { + c.Call = c.Call.DoAndReturn(f) + return c +} + +// ProjectIDCondition mocks base method. +func (m *MockAuthorizationRepository) ProjectIDCondition(projectID string) database.Condition { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "ProjectIDCondition", projectID) + ret0, _ := ret[0].(database.Condition) + return ret0 +} + +// ProjectIDCondition indicates an expected call of ProjectIDCondition. +func (mr *MockAuthorizationRepositoryMockRecorder) ProjectIDCondition(projectID any) *MockAuthorizationRepositoryProjectIDConditionCall { + mr.mock.ctrl.T.Helper() + call := mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ProjectIDCondition", reflect.TypeOf((*MockAuthorizationRepository)(nil).ProjectIDCondition), projectID) + return &MockAuthorizationRepositoryProjectIDConditionCall{Call: call} +} + +// MockAuthorizationRepositoryProjectIDConditionCall wrap *gomock.Call +type MockAuthorizationRepositoryProjectIDConditionCall struct { + *gomock.Call +} + +// Return rewrite *gomock.Call.Return +func (c *MockAuthorizationRepositoryProjectIDConditionCall) Return(arg0 database.Condition) *MockAuthorizationRepositoryProjectIDConditionCall { + c.Call = c.Call.Return(arg0) + return c +} + +// Do rewrite *gomock.Call.Do +func (c *MockAuthorizationRepositoryProjectIDConditionCall) Do(f func(string) database.Condition) *MockAuthorizationRepositoryProjectIDConditionCall { + c.Call = c.Call.Do(f) + return c +} + +// DoAndReturn rewrite *gomock.Call.DoAndReturn +func (c *MockAuthorizationRepositoryProjectIDConditionCall) DoAndReturn(f func(string) database.Condition) *MockAuthorizationRepositoryProjectIDConditionCall { + c.Call = c.Call.DoAndReturn(f) + return c +} + +// RoleCondition mocks base method. +func (m *MockAuthorizationRepository) RoleCondition(op database.TextOperation, role string) database.Condition { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "RoleCondition", op, role) + ret0, _ := ret[0].(database.Condition) + return ret0 +} + +// RoleCondition indicates an expected call of RoleCondition. +func (mr *MockAuthorizationRepositoryMockRecorder) RoleCondition(op, role any) *MockAuthorizationRepositoryRoleConditionCall { + mr.mock.ctrl.T.Helper() + call := mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "RoleCondition", reflect.TypeOf((*MockAuthorizationRepository)(nil).RoleCondition), op, role) + return &MockAuthorizationRepositoryRoleConditionCall{Call: call} +} + +// MockAuthorizationRepositoryRoleConditionCall wrap *gomock.Call +type MockAuthorizationRepositoryRoleConditionCall struct { + *gomock.Call +} + +// Return rewrite *gomock.Call.Return +func (c *MockAuthorizationRepositoryRoleConditionCall) Return(arg0 database.Condition) *MockAuthorizationRepositoryRoleConditionCall { + c.Call = c.Call.Return(arg0) + return c +} + +// Do rewrite *gomock.Call.Do +func (c *MockAuthorizationRepositoryRoleConditionCall) Do(f func(database.TextOperation, string) database.Condition) *MockAuthorizationRepositoryRoleConditionCall { + c.Call = c.Call.Do(f) + return c +} + +// DoAndReturn rewrite *gomock.Call.DoAndReturn +func (c *MockAuthorizationRepositoryRoleConditionCall) DoAndReturn(f func(database.TextOperation, string) database.Condition) *MockAuthorizationRepositoryRoleConditionCall { + c.Call = c.Call.DoAndReturn(f) + return c +} + +// SetState mocks base method. +func (m *MockAuthorizationRepository) SetState(state domain.AuthorizationState) database.Change { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "SetState", state) + ret0, _ := ret[0].(database.Change) + return ret0 +} + +// SetState indicates an expected call of SetState. +func (mr *MockAuthorizationRepositoryMockRecorder) SetState(state any) *MockAuthorizationRepositorySetStateCall { + mr.mock.ctrl.T.Helper() + call := mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "SetState", reflect.TypeOf((*MockAuthorizationRepository)(nil).SetState), state) + return &MockAuthorizationRepositorySetStateCall{Call: call} +} + +// MockAuthorizationRepositorySetStateCall wrap *gomock.Call +type MockAuthorizationRepositorySetStateCall struct { + *gomock.Call +} + +// Return rewrite *gomock.Call.Return +func (c *MockAuthorizationRepositorySetStateCall) Return(arg0 database.Change) *MockAuthorizationRepositorySetStateCall { + c.Call = c.Call.Return(arg0) + return c +} + +// Do rewrite *gomock.Call.Do +func (c *MockAuthorizationRepositorySetStateCall) Do(f func(domain.AuthorizationState) database.Change) *MockAuthorizationRepositorySetStateCall { + c.Call = c.Call.Do(f) + return c +} + +// DoAndReturn rewrite *gomock.Call.DoAndReturn +func (c *MockAuthorizationRepositorySetStateCall) DoAndReturn(f func(domain.AuthorizationState) database.Change) *MockAuthorizationRepositorySetStateCall { + c.Call = c.Call.DoAndReturn(f) + return c +} + +// SetUpdatedAt mocks base method. +func (m *MockAuthorizationRepository) SetUpdatedAt(updatedAt time.Time) database.Change { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "SetUpdatedAt", updatedAt) + ret0, _ := ret[0].(database.Change) + return ret0 +} + +// SetUpdatedAt indicates an expected call of SetUpdatedAt. +func (mr *MockAuthorizationRepositoryMockRecorder) SetUpdatedAt(updatedAt any) *MockAuthorizationRepositorySetUpdatedAtCall { + mr.mock.ctrl.T.Helper() + call := mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "SetUpdatedAt", reflect.TypeOf((*MockAuthorizationRepository)(nil).SetUpdatedAt), updatedAt) + return &MockAuthorizationRepositorySetUpdatedAtCall{Call: call} +} + +// MockAuthorizationRepositorySetUpdatedAtCall wrap *gomock.Call +type MockAuthorizationRepositorySetUpdatedAtCall struct { + *gomock.Call +} + +// Return rewrite *gomock.Call.Return +func (c *MockAuthorizationRepositorySetUpdatedAtCall) Return(arg0 database.Change) *MockAuthorizationRepositorySetUpdatedAtCall { + c.Call = c.Call.Return(arg0) + return c +} + +// Do rewrite *gomock.Call.Do +func (c *MockAuthorizationRepositorySetUpdatedAtCall) Do(f func(time.Time) database.Change) *MockAuthorizationRepositorySetUpdatedAtCall { + c.Call = c.Call.Do(f) + return c +} + +// DoAndReturn rewrite *gomock.Call.DoAndReturn +func (c *MockAuthorizationRepositorySetUpdatedAtCall) DoAndReturn(f func(time.Time) database.Change) *MockAuthorizationRepositorySetUpdatedAtCall { + c.Call = c.Call.DoAndReturn(f) + return c +} + +// StateColumn mocks base method. +func (m *MockAuthorizationRepository) StateColumn() database.Column { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "StateColumn") + ret0, _ := ret[0].(database.Column) + return ret0 +} + +// StateColumn indicates an expected call of StateColumn. +func (mr *MockAuthorizationRepositoryMockRecorder) StateColumn() *MockAuthorizationRepositoryStateColumnCall { + mr.mock.ctrl.T.Helper() + call := mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "StateColumn", reflect.TypeOf((*MockAuthorizationRepository)(nil).StateColumn)) + return &MockAuthorizationRepositoryStateColumnCall{Call: call} +} + +// MockAuthorizationRepositoryStateColumnCall wrap *gomock.Call +type MockAuthorizationRepositoryStateColumnCall struct { + *gomock.Call +} + +// Return rewrite *gomock.Call.Return +func (c *MockAuthorizationRepositoryStateColumnCall) Return(arg0 database.Column) *MockAuthorizationRepositoryStateColumnCall { + c.Call = c.Call.Return(arg0) + return c +} + +// Do rewrite *gomock.Call.Do +func (c *MockAuthorizationRepositoryStateColumnCall) Do(f func() database.Column) *MockAuthorizationRepositoryStateColumnCall { + c.Call = c.Call.Do(f) + return c +} + +// DoAndReturn rewrite *gomock.Call.DoAndReturn +func (c *MockAuthorizationRepositoryStateColumnCall) DoAndReturn(f func() database.Column) *MockAuthorizationRepositoryStateColumnCall { + c.Call = c.Call.DoAndReturn(f) + return c +} + +// StateCondition mocks base method. +func (m *MockAuthorizationRepository) StateCondition(state domain.AuthorizationState) database.Condition { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "StateCondition", state) + ret0, _ := ret[0].(database.Condition) + return ret0 +} + +// StateCondition indicates an expected call of StateCondition. +func (mr *MockAuthorizationRepositoryMockRecorder) StateCondition(state any) *MockAuthorizationRepositoryStateConditionCall { + mr.mock.ctrl.T.Helper() + call := mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "StateCondition", reflect.TypeOf((*MockAuthorizationRepository)(nil).StateCondition), state) + return &MockAuthorizationRepositoryStateConditionCall{Call: call} +} + +// MockAuthorizationRepositoryStateConditionCall wrap *gomock.Call +type MockAuthorizationRepositoryStateConditionCall struct { + *gomock.Call +} + +// Return rewrite *gomock.Call.Return +func (c *MockAuthorizationRepositoryStateConditionCall) Return(arg0 database.Condition) *MockAuthorizationRepositoryStateConditionCall { + c.Call = c.Call.Return(arg0) + return c +} + +// Do rewrite *gomock.Call.Do +func (c *MockAuthorizationRepositoryStateConditionCall) Do(f func(domain.AuthorizationState) database.Condition) *MockAuthorizationRepositoryStateConditionCall { + c.Call = c.Call.Do(f) + return c +} + +// DoAndReturn rewrite *gomock.Call.DoAndReturn +func (c *MockAuthorizationRepositoryStateConditionCall) DoAndReturn(f func(domain.AuthorizationState) database.Condition) *MockAuthorizationRepositoryStateConditionCall { + c.Call = c.Call.DoAndReturn(f) + return c +} + +// Update mocks base method. +func (m *MockAuthorizationRepository) Update(ctx context.Context, client database.QueryExecutor, condition database.Condition, roles []string, changes ...database.Change) (int64, error) { + m.ctrl.T.Helper() + varargs := []any{ctx, client, condition, roles} + for _, a := range changes { + varargs = append(varargs, a) + } + ret := m.ctrl.Call(m, "Update", varargs...) + ret0, _ := ret[0].(int64) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// Update indicates an expected call of Update. +func (mr *MockAuthorizationRepositoryMockRecorder) Update(ctx, client, condition, roles any, changes ...any) *MockAuthorizationRepositoryUpdateCall { + mr.mock.ctrl.T.Helper() + varargs := append([]any{ctx, client, condition, roles}, changes...) + call := mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Update", reflect.TypeOf((*MockAuthorizationRepository)(nil).Update), varargs...) + return &MockAuthorizationRepositoryUpdateCall{Call: call} +} + +// MockAuthorizationRepositoryUpdateCall wrap *gomock.Call +type MockAuthorizationRepositoryUpdateCall struct { + *gomock.Call +} + +// Return rewrite *gomock.Call.Return +func (c *MockAuthorizationRepositoryUpdateCall) Return(arg0 int64, arg1 error) *MockAuthorizationRepositoryUpdateCall { + c.Call = c.Call.Return(arg0, arg1) + return c +} + +// Do rewrite *gomock.Call.Do +func (c *MockAuthorizationRepositoryUpdateCall) Do(f func(context.Context, database.QueryExecutor, database.Condition, []string, ...database.Change) (int64, error)) *MockAuthorizationRepositoryUpdateCall { + c.Call = c.Call.Do(f) + return c +} + +// DoAndReturn rewrite *gomock.Call.DoAndReturn +func (c *MockAuthorizationRepositoryUpdateCall) DoAndReturn(f func(context.Context, database.QueryExecutor, database.Condition, []string, ...database.Change) (int64, error)) *MockAuthorizationRepositoryUpdateCall { + c.Call = c.Call.DoAndReturn(f) + return c +} + +// UpdatedAtColumn mocks base method. +func (m *MockAuthorizationRepository) UpdatedAtColumn() database.Column { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "UpdatedAtColumn") + ret0, _ := ret[0].(database.Column) + return ret0 +} + +// UpdatedAtColumn indicates an expected call of UpdatedAtColumn. +func (mr *MockAuthorizationRepositoryMockRecorder) UpdatedAtColumn() *MockAuthorizationRepositoryUpdatedAtColumnCall { + mr.mock.ctrl.T.Helper() + call := mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "UpdatedAtColumn", reflect.TypeOf((*MockAuthorizationRepository)(nil).UpdatedAtColumn)) + return &MockAuthorizationRepositoryUpdatedAtColumnCall{Call: call} +} + +// MockAuthorizationRepositoryUpdatedAtColumnCall wrap *gomock.Call +type MockAuthorizationRepositoryUpdatedAtColumnCall struct { + *gomock.Call +} + +// Return rewrite *gomock.Call.Return +func (c *MockAuthorizationRepositoryUpdatedAtColumnCall) Return(arg0 database.Column) *MockAuthorizationRepositoryUpdatedAtColumnCall { + c.Call = c.Call.Return(arg0) + return c +} + +// Do rewrite *gomock.Call.Do +func (c *MockAuthorizationRepositoryUpdatedAtColumnCall) Do(f func() database.Column) *MockAuthorizationRepositoryUpdatedAtColumnCall { + c.Call = c.Call.Do(f) + return c +} + +// DoAndReturn rewrite *gomock.Call.DoAndReturn +func (c *MockAuthorizationRepositoryUpdatedAtColumnCall) DoAndReturn(f func() database.Column) *MockAuthorizationRepositoryUpdatedAtColumnCall { + c.Call = c.Call.DoAndReturn(f) + return c +} + +// UserIDColumn mocks base method. +func (m *MockAuthorizationRepository) UserIDColumn() database.Column { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "UserIDColumn") + ret0, _ := ret[0].(database.Column) + return ret0 +} + +// UserIDColumn indicates an expected call of UserIDColumn. +func (mr *MockAuthorizationRepositoryMockRecorder) UserIDColumn() *MockAuthorizationRepositoryUserIDColumnCall { + mr.mock.ctrl.T.Helper() + call := mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "UserIDColumn", reflect.TypeOf((*MockAuthorizationRepository)(nil).UserIDColumn)) + return &MockAuthorizationRepositoryUserIDColumnCall{Call: call} +} + +// MockAuthorizationRepositoryUserIDColumnCall wrap *gomock.Call +type MockAuthorizationRepositoryUserIDColumnCall struct { + *gomock.Call +} + +// Return rewrite *gomock.Call.Return +func (c *MockAuthorizationRepositoryUserIDColumnCall) Return(arg0 database.Column) *MockAuthorizationRepositoryUserIDColumnCall { + c.Call = c.Call.Return(arg0) + return c +} + +// Do rewrite *gomock.Call.Do +func (c *MockAuthorizationRepositoryUserIDColumnCall) Do(f func() database.Column) *MockAuthorizationRepositoryUserIDColumnCall { + c.Call = c.Call.Do(f) + return c +} + +// DoAndReturn rewrite *gomock.Call.DoAndReturn +func (c *MockAuthorizationRepositoryUserIDColumnCall) DoAndReturn(f func() database.Column) *MockAuthorizationRepositoryUserIDColumnCall { + c.Call = c.Call.DoAndReturn(f) + return c +} + +// UserIDCondition mocks base method. +func (m *MockAuthorizationRepository) UserIDCondition(userID string) database.Condition { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "UserIDCondition", userID) + ret0, _ := ret[0].(database.Condition) + return ret0 +} + +// UserIDCondition indicates an expected call of UserIDCondition. +func (mr *MockAuthorizationRepositoryMockRecorder) UserIDCondition(userID any) *MockAuthorizationRepositoryUserIDConditionCall { + mr.mock.ctrl.T.Helper() + call := mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "UserIDCondition", reflect.TypeOf((*MockAuthorizationRepository)(nil).UserIDCondition), userID) + return &MockAuthorizationRepositoryUserIDConditionCall{Call: call} +} + +// MockAuthorizationRepositoryUserIDConditionCall wrap *gomock.Call +type MockAuthorizationRepositoryUserIDConditionCall struct { + *gomock.Call +} + +// Return rewrite *gomock.Call.Return +func (c *MockAuthorizationRepositoryUserIDConditionCall) Return(arg0 database.Condition) *MockAuthorizationRepositoryUserIDConditionCall { + c.Call = c.Call.Return(arg0) + return c +} + +// Do rewrite *gomock.Call.Do +func (c *MockAuthorizationRepositoryUserIDConditionCall) Do(f func(string) database.Condition) *MockAuthorizationRepositoryUserIDConditionCall { + c.Call = c.Call.Do(f) + return c +} + +// DoAndReturn rewrite *gomock.Call.DoAndReturn +func (c *MockAuthorizationRepositoryUserIDConditionCall) DoAndReturn(f func(string) database.Condition) *MockAuthorizationRepositoryUserIDConditionCall { + c.Call = c.Call.DoAndReturn(f) + return c +} diff --git a/backend/v3/storage/database/dialect/postgres/migration/010_authorizations_table.go b/backend/v3/storage/database/dialect/postgres/migration/010_authorizations_table.go new file mode 100644 index 000000000..8315d478a --- /dev/null +++ b/backend/v3/storage/database/dialect/postgres/migration/010_authorizations_table.go @@ -0,0 +1,16 @@ +package migration + +import ( + _ "embed" +) + +var ( + //go:embed 010_authorizations_table/up.sql + up010AuthorizationsTable string + //go:embed 010_authorizations_table/down.sql + down010AuthorizationsTable string +) + +func init() { + registerSQLMigration(10, up010AuthorizationsTable, down010AuthorizationsTable) +} diff --git a/backend/v3/storage/database/dialect/postgres/migration/010_authorizations_table/down.sql b/backend/v3/storage/database/dialect/postgres/migration/010_authorizations_table/down.sql new file mode 100644 index 000000000..036e4e9e7 --- /dev/null +++ b/backend/v3/storage/database/dialect/postgres/migration/010_authorizations_table/down.sql @@ -0,0 +1,3 @@ +DROP TABLE IF EXISTS zitadel.authorization_roles; +DROP TABLE IF EXISTS zitadel.authorizations; +DROP TYPE IF EXISTS zitadel.authorization_state; \ No newline at end of file diff --git a/backend/v3/storage/database/dialect/postgres/migration/010_authorizations_table/up.sql b/backend/v3/storage/database/dialect/postgres/migration/010_authorizations_table/up.sql new file mode 100644 index 000000000..7b4380f68 --- /dev/null +++ b/backend/v3/storage/database/dialect/postgres/migration/010_authorizations_table/up.sql @@ -0,0 +1,40 @@ +CREATE TYPE zitadel.authorization_state AS ENUM ( + 'active', + 'inactive' + ); + +CREATE TABLE zitadel.authorizations +( + instance_id TEXT NOT NULL, + id TEXT NOT NULL CHECK ( id <> '' ), + state zitadel.authorization_state NOT NULL DEFAULT 'active', + project_id TEXT NOT NULL, + grant_id TEXT, + user_id TEXT NOT NULL, + created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), + updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), + PRIMARY KEY (instance_id, id), +-- FOREIGN KEY (instance_id, user_id) REFERENCES zitadel.users (instance_id, id) ON DELETE CASCADE, + FOREIGN KEY (instance_id, project_id) REFERENCES zitadel.projects (instance_id, id) ON DELETE CASCADE, + FOREIGN KEY (instance_id, grant_id) REFERENCES zitadel.project_grants (instance_id, id) ON DELETE CASCADE +); + +CREATE TABLE zitadel.authorization_roles +( + instance_id TEXT NOT NULL, + authorization_id TEXT NOT NULL, + role_key TEXT NOT NULL CHECK ( role_key <> '' ), + project_id TEXT NOT NULL, + grant_id TEXT, + PRIMARY KEY (instance_id, authorization_id, role_key), + FOREIGN KEY (instance_id, authorization_id) REFERENCES zitadel.authorizations (instance_id, id) ON DELETE CASCADE, + FOREIGN KEY (instance_id, project_id, role_key) REFERENCES zitadel.project_roles (instance_id, project_id, key) ON DELETE CASCADE, + FOREIGN KEY (instance_id, grant_id, role_key) REFERENCES zitadel.project_grant_roles (instance_id, grant_id, key) ON DELETE CASCADE +); + +CREATE TRIGGER trigger_set_updated_at + BEFORE UPDATE + ON zitadel.authorizations + FOR EACH ROW + WHEN (NEW.updated_at IS NULL) +EXECUTE FUNCTION zitadel.set_updated_at(); \ No newline at end of file diff --git a/backend/v3/storage/database/events_testing/authorization_test.go b/backend/v3/storage/database/events_testing/authorization_test.go new file mode 100644 index 000000000..e53b6f224 --- /dev/null +++ b/backend/v3/storage/database/events_testing/authorization_test.go @@ -0,0 +1,505 @@ +//go:build integration + +package events_test + +import ( + "testing" + "time" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + + "github.com/zitadel/zitadel/backend/v3/domain" + "github.com/zitadel/zitadel/backend/v3/storage/database" + "github.com/zitadel/zitadel/backend/v3/storage/database/repository" + "github.com/zitadel/zitadel/internal/integration" + authorization_v2 "github.com/zitadel/zitadel/pkg/grpc/authorization/v2" + instance_v2 "github.com/zitadel/zitadel/pkg/grpc/instance/v2" + org_v2beta "github.com/zitadel/zitadel/pkg/grpc/org/v2beta" + project_v2beta "github.com/zitadel/zitadel/pkg/grpc/project/v2beta" + user_v2 "github.com/zitadel/zitadel/pkg/grpc/user/v2" +) + +func TestServer_AuthorizationReduces(t *testing.T) { + instanceID := Instance.ID() + orgID := Instance.DefaultOrg.Id + authorizationRepo := repository.AuthorizationRepository() + + retryDuration, tick := integration.WaitForAndTickWithMaxDuration(CTX, time.Minute) + + t.Run("user grant added reduces", func(t *testing.T) { + // create user + user := Instance.CreateHumanUserVerified(CTX, orgID, integration.Email(), integration.Phone()) + // prepare project and project roles + role1, role2 := "role1", "role2" + projectID := prepareProjectAndProjectRoles(t, orgID, []string{role1, role2}) + createAndEnsureAuthorization(t, instanceID, orgID, user.UserId, projectID, []string{role1, role2}, retryDuration, tick) + }) + + t.Run("user grant update reduces", func(t *testing.T) { + // create user + user := Instance.CreateHumanUserVerified(CTX, orgID, integration.Email(), integration.Phone()) + // prepare project and project roles + role1, role2 := "role1", "role2" + projectID := prepareProjectAndProjectRoles(t, orgID, []string{role1, role2}) + + beforeCreate := time.Now() + // create authorization + createdAuthorization, err := AuthorizationClient.CreateAuthorization(CTX, &authorization_v2.CreateAuthorizationRequest{ + UserId: user.UserId, + ProjectId: projectID, + OrganizationId: orgID, + RoleKeys: []string{role1, role2}, + }) + require.NoError(t, err) + afterCreate := time.Now().Add(500 * time.Millisecond) + + // add a new role to the project + role3 := "role3" + _, err = ProjectClient.AddProjectRole(CTX, &project_v2beta.AddProjectRoleRequest{ + ProjectId: projectID, + RoleKey: role3, + DisplayName: "display", + Group: nil, + }) + require.NoError(t, err) + + beforeUpdate := time.Now() + // update roles to [role1, role2, role3] + _, err = AuthorizationClient.UpdateAuthorization(CTX, &authorization_v2.UpdateAuthorizationRequest{ + Id: createdAuthorization.Id, + RoleKeys: []string{role1, role2, role3}, + }) + require.NoError(t, err) + afterUpdate := time.Now().Add(500 * time.Millisecond) + + require.EventuallyWithT(t, func(collect *assert.CollectT) { + az, err := authorizationRepo.Get(CTX, pool, database.WithCondition( + authorizationRepo.PrimaryKeyCondition(instanceID, createdAuthorization.Id), + )) + require.NoError(collect, err) + require.Len(collect, az.Roles, 3) + assert.Equal(collect, []string{role1, role2, role3}, az.Roles) + assert.WithinRange(collect, az.UpdatedAt, beforeUpdate, afterUpdate) + assert.WithinRange(collect, az.CreatedAt, beforeCreate, afterCreate) + }, retryDuration, tick, "authorization not updated within %v: %v", retryDuration, err) + }) + + t.Run("user grant deactivate reduces", func(t *testing.T) { + // create user + user := Instance.CreateHumanUserVerified(CTX, orgID, integration.Email(), integration.Phone()) + // prepare project and project roles + projectID := prepareProjectAndProjectRoles(t, orgID, nil) + // create authorization + beforeCreate := time.Now() + createdAuthorization, err := AuthorizationClient.CreateAuthorization(CTX, &authorization_v2.CreateAuthorizationRequest{ + UserId: user.UserId, + ProjectId: projectID, + OrganizationId: orgID, + }) + require.NoError(t, err) + afterCreate := time.Now().Add(500 * time.Millisecond) + + before := time.Now() + // deactivate authorization + _, err = AuthorizationClient.DeactivateAuthorization(CTX, &authorization_v2.DeactivateAuthorizationRequest{ + Id: createdAuthorization.Id, + }) + require.NoError(t, err) + after := time.Now().Add(500 * time.Millisecond) + + require.EventuallyWithT(t, func(collect *assert.CollectT) { + az, err := authorizationRepo.Get(CTX, pool, database.WithCondition( + authorizationRepo.PrimaryKeyCondition(instanceID, createdAuthorization.Id), + )) + require.NoError(collect, err) + assert.Equal(collect, domain.AuthorizationStateInactive, az.State) + assert.WithinRange(collect, az.UpdatedAt, before, after) + assert.WithinRange(collect, az.CreatedAt, beforeCreate, afterCreate) + }, retryDuration, tick, "authorization not deactivated within %v: %v", retryDuration, err) + }) + + t.Run("user grant activate reduces", func(t *testing.T) { + // create user + user := Instance.CreateHumanUserVerified(CTX, orgID, integration.Email(), integration.Phone()) + // prepare project + projectID := prepareProjectAndProjectRoles(t, orgID, nil) + // create authorization + beforeCreate := time.Now() + createdAuthorization, err := AuthorizationClient.CreateAuthorization(CTX, &authorization_v2.CreateAuthorizationRequest{ + UserId: user.UserId, + ProjectId: projectID, + OrganizationId: orgID, + }) + require.NoError(t, err) + afterCreate := time.Now().Add(500 * time.Millisecond) + + // deactivate authorization + _, err = AuthorizationClient.DeactivateAuthorization(CTX, &authorization_v2.DeactivateAuthorizationRequest{ + Id: createdAuthorization.Id, + }) + require.NoError(t, err) + + before := time.Now() + // re-activate authorization + _, err = AuthorizationClient.ActivateAuthorization(CTX, &authorization_v2.ActivateAuthorizationRequest{ + Id: createdAuthorization.Id, + }) + require.NoError(t, err) + after := time.Now().Add(500 * time.Millisecond) + + require.EventuallyWithT(t, func(collect *assert.CollectT) { + az, err := authorizationRepo.Get(CTX, pool, database.WithCondition( + authorizationRepo.PrimaryKeyCondition(instanceID, createdAuthorization.Id), + )) + require.NoError(collect, err) + assert.Equal(collect, domain.AuthorizationStateActive, az.State) + assert.WithinRange(collect, az.UpdatedAt, before, after) + assert.WithinRange(collect, az.CreatedAt, beforeCreate, afterCreate) + }, retryDuration, tick, "authorization not activated within %v: %v", retryDuration, err) + }) + + t.Run("user grant removed reduces", func(t *testing.T) { + // create user + user := Instance.CreateHumanUserVerified(CTX, orgID, integration.Email(), integration.Phone()) + // prepare project and project roles + role1, role2 := "role1", "role2" + projectID := prepareProjectAndProjectRoles(t, orgID, []string{role1, role2}) + + // create authorization with roles + createdAuthorization, err := AuthorizationClient.CreateAuthorization(CTX, &authorization_v2.CreateAuthorizationRequest{ + UserId: user.UserId, + ProjectId: projectID, + RoleKeys: []string{role1, role2}, + OrganizationId: orgID, + }) + require.NoError(t, err) + + // delete authorization + _, err = AuthorizationClient.DeleteAuthorization(CTX, &authorization_v2.DeleteAuthorizationRequest{ + Id: createdAuthorization.Id, + }) + require.NoError(t, err) + + require.EventuallyWithT(t, func(collect *assert.CollectT) { + az, err := authorizationRepo.Get(CTX, pool, database.WithCondition( + authorizationRepo.PrimaryKeyCondition(instanceID, createdAuthorization.Id), + )) + require.Empty(collect, az) + require.ErrorIs(collect, err, new(database.NoRowFoundError)) + }, retryDuration, tick, "authorization not deleted within %v: %v", retryDuration, err) + }) + + t.Run("user removed reduces", func(t *testing.T) { + // create user + user := Instance.CreateHumanUserVerified(CTX, orgID, integration.Email(), integration.Phone()) + // prepare project and project roles + role1, role2 := "role1", "role2" + projectID := prepareProjectAndProjectRoles(t, orgID, []string{role1, role2}) + // create authorization with roles + createdAuthorization := createAndEnsureAuthorization(t, instanceID, orgID, user.UserId, projectID, []string{role1, role2}, retryDuration, tick) + // delete the user + _, err := UserClient.DeleteUser(CTX, &user_v2.DeleteUserRequest{ + UserId: user.UserId, + }) + require.NoError(t, err) + + // ensure authorization is deleted + require.EventuallyWithT(t, func(collect *assert.CollectT) { + az, err := authorizationRepo.Get(CTX, pool, database.WithCondition( + authorizationRepo.PrimaryKeyCondition(instanceID, createdAuthorization.Id), + )) + require.Empty(collect, az) + require.ErrorIs(collect, err, new(database.NoRowFoundError)) + }, retryDuration, tick, "authorization not deleted within %v: %v", retryDuration, err) + }) + + t.Run("project removed reduces", func(t *testing.T) { + // create user + user := Instance.CreateHumanUserVerified(CTX, orgID, integration.Email(), integration.Phone()) + // prepare project and project roles + role1, role2 := "role1", "role2" + projectID := prepareProjectAndProjectRoles(t, orgID, []string{role1, role2}) + // create authorization with roles + createdAuthorization := createAndEnsureAuthorization(t, instanceID, orgID, user.UserId, projectID, []string{role1, role2}, retryDuration, tick) + // delete the project + _, err := ProjectClient.DeleteProject(CTX, &project_v2beta.DeleteProjectRequest{ + Id: projectID, + }) + require.NoError(t, err) + + // ensure authorization is deleted + require.EventuallyWithT(t, func(collect *assert.CollectT) { + az, err := authorizationRepo.Get(CTX, pool, database.WithCondition( + authorizationRepo.PrimaryKeyCondition(instanceID, createdAuthorization.Id), + )) + require.Empty(collect, az) + require.ErrorIs(collect, err, new(database.NoRowFoundError)) + }, retryDuration, tick, "authorization not deleted within %v: %v", retryDuration, err) + }) + + t.Run("project role removed reduces", func(t *testing.T) { + // create user + user := Instance.CreateHumanUserVerified(CTX, orgID, integration.Email(), integration.Phone()) + // prepare project and project roles + role1, role2 := "role1", "role2" + projectID := prepareProjectAndProjectRoles(t, orgID, []string{role1, role2}) + // create authorization with roles + createdAuthorization := createAndEnsureAuthorization(t, instanceID, orgID, user.UserId, projectID, []string{role1, role2}, retryDuration, tick) + // delete a project role + before := time.Now() + _, err := ProjectClient.RemoveProjectRole(CTX, &project_v2beta.RemoveProjectRoleRequest{ + ProjectId: projectID, + RoleKey: role2, + }) + require.NoError(t, err) + after := time.Now() + + // ensure authorization is updated + require.EventuallyWithT(t, func(collect *assert.CollectT) { + az, err := authorizationRepo.Get(CTX, pool, database.WithCondition( + authorizationRepo.PrimaryKeyCondition(instanceID, createdAuthorization.Id), + )) + require.NoError(collect, err) + assert.Equal(collect, []string{role1}, az.Roles) + assert.Equal(collect, domain.AuthorizationStateActive, az.State) + assert.WithinRange(collect, az.UpdatedAt, before, after) + }, retryDuration, tick, "authorization not updated within %v: %v", retryDuration, err) + }) + + t.Run("user grant added for a project grant reduces", func(t *testing.T) { + // prepare project and project roles + role1 := "role1" + projectID := prepareProjectAndProjectRoles(t, orgID, []string{role1}) + + // granted organization + grantedOrganizationName := integration.OrganizationName() + grantedOrganization := Instance.CreateOrganization(CTX, grantedOrganizationName, integration.Email()) + + // create project grant + _, err := ProjectClient.CreateProjectGrant(CTX, &project_v2beta.CreateProjectGrantRequest{ + ProjectId: projectID, + RoleKeys: []string{role1}, + GrantedOrganizationId: grantedOrganization.OrganizationId, + }) + require.NoError(t, err) + + // create user + user := Instance.CreateHumanUserVerified(CTX, grantedOrganization.OrganizationId, integration.Email(), integration.Phone()) + // create authorization with roles + createAndEnsureAuthorization(t, instanceID, grantedOrganization.OrganizationId, user.UserId, projectID, []string{role1}, retryDuration, tick) + }) + + t.Run("project grant removed reduces", func(t *testing.T) { + // prepare project and project roles + role1 := "role1" + projectID := prepareProjectAndProjectRoles(t, orgID, []string{role1}) + + // granted organization + grantedOrganizationName := integration.OrganizationName() + grantedOrganization := Instance.CreateOrganization(CTX, grantedOrganizationName, integration.Email()) + + // create project grant + _, err := ProjectClient.CreateProjectGrant(CTX, &project_v2beta.CreateProjectGrantRequest{ + ProjectId: projectID, + RoleKeys: []string{role1}, + GrantedOrganizationId: grantedOrganization.OrganizationId, + }) + require.NoError(t, err) + // create user + user := Instance.CreateHumanUserVerified(CTX, grantedOrganization.OrganizationId, integration.Email(), integration.Phone()) + // create authorization with roles + createdAuthorization := createAndEnsureAuthorization(t, instanceID, grantedOrganization.OrganizationId, user.UserId, projectID, []string{role1}, retryDuration, tick) + // delete project grant + _, err = ProjectClient.DeleteProjectGrant(CTX, &project_v2beta.DeleteProjectGrantRequest{ + ProjectId: projectID, + GrantedOrganizationId: grantedOrganization.OrganizationId, + }) + require.NoError(t, err) + + // ensure authorization is deleted + require.EventuallyWithT(t, func(collect *assert.CollectT) { + az, err := authorizationRepo.Get(CTX, pool, database.WithCondition( + authorizationRepo.PrimaryKeyCondition(instanceID, createdAuthorization.Id), + )) + require.Empty(collect, az) + require.ErrorIs(collect, err, new(database.NoRowFoundError)) + }, retryDuration, tick, "authorization not deleted within %v: %v", retryDuration, err) + }) + + t.Run("project grant updated reduces", func(t *testing.T) { + // prepare project and project roles + role1, role2 := "role1", "role2" + projectID := prepareProjectAndProjectRoles(t, orgID, []string{role1, role2}) + // granted organization + grantedOrganizationName := integration.OrganizationName() + grantedOrganization := Instance.CreateOrganization(CTX, grantedOrganizationName, integration.Email()) + // create project grant + _, err := ProjectClient.CreateProjectGrant(CTX, &project_v2beta.CreateProjectGrantRequest{ + ProjectId: projectID, + RoleKeys: []string{role1, role2}, + GrantedOrganizationId: grantedOrganization.OrganizationId, + }) + require.NoError(t, err) + // create user + user := Instance.CreateHumanUserVerified(CTX, grantedOrganization.OrganizationId, integration.Email(), integration.Phone()) + // create authorization with roles + createdAuthorization := createAndEnsureAuthorization(t, instanceID, grantedOrganization.OrganizationId, user.UserId, projectID, []string{role1, role2}, retryDuration, tick) + // update project grant + //beforeUpdate := time.Now() + _, err = ProjectClient.UpdateProjectGrant(CTX, &project_v2beta.UpdateProjectGrantRequest{ + ProjectId: projectID, + GrantedOrganizationId: grantedOrganization.OrganizationId, + RoleKeys: []string{role1}, + }) + require.NoError(t, err) + //afterUpdate := time.Now().Add(500 * time.Millisecond) // add some buffer for eventual consistency of cascading changes + + // ensure authorization is updated + require.EventuallyWithT(t, func(collect *assert.CollectT) { + az, err := authorizationRepo.Get(CTX, pool, database.WithCondition( + authorizationRepo.PrimaryKeyCondition(instanceID, createdAuthorization.Id), + )) + require.NoError(collect, err) + assert.Equal(collect, []string{role1}, az.Roles) + //assert.WithinRange(collect, az.UpdatedAt, beforeUpdate, afterUpdate) + }, retryDuration, tick, "authorization not updated within %v: %v", retryDuration, err) + }) + + t.Run("org removed reduces", func(t *testing.T) { + // create a new organization + orgName := integration.OrganizationName() + orgResp := Instance.CreateOrganization(CTX, orgName, integration.Email()) + // create user + user := Instance.CreateHumanUserVerified(CTX, orgResp.OrganizationId, integration.Email(), integration.Phone()) + // prepare project and project roles + role1, role2 := "role1", "role2" + projectID := prepareProjectAndProjectRoles(t, orgResp.OrganizationId, []string{role1, role2}) + // create authorization with roles + createdAuthorization := createAndEnsureAuthorization(t, instanceID, orgResp.OrganizationId, user.UserId, projectID, []string{role1, role2}, retryDuration, tick) + // delete the organization + _, err := OrgClient.DeleteOrganization(CTX, &org_v2beta.DeleteOrganizationRequest{ + Id: orgResp.OrganizationId, + }) + require.NoError(t, err) + + // ensure authorization is deleted + require.EventuallyWithT(t, func(collect *assert.CollectT) { + az, err := authorizationRepo.Get(CTX, pool, database.WithCondition( + authorizationRepo.PrimaryKeyCondition(instanceID, createdAuthorization.Id), + )) + require.Empty(collect, az) + require.ErrorIs(collect, err, new(database.NoRowFoundError)) + }, retryDuration, tick, "authorization not deleted within %v: %v", retryDuration, err) + }) + + t.Run("instance removed reduces", func(t *testing.T) { + // create a new instance + instance := integration.NewInstance(CTX) + // create a new organization + orgResp := instance.CreateOrganization(CTX, integration.OrganizationName(), integration.Email()) + // create a user + user := instance.CreateHumanUserVerified(CTX, orgResp.OrganizationId, integration.Email(), integration.Phone()) + // prepare project and project roles + role1, role2 := "role1", "role2" + projectResp := instance.CreateProject(CTX, t, orgResp.OrganizationId, integration.ProjectName(), false, false) + _ = instance.AddProjectRole(CTX, t, projectResp.Id, role1, "display", "") + _ = instance.AddProjectRole(CTX, t, projectResp.Id, role2, "display", "") + + // create authorization with roles + beforeCreate := time.Now() + createdAuthorization, err := instance.Client.AuthorizationV2.CreateAuthorization(CTX, &authorization_v2.CreateAuthorizationRequest{ + UserId: user.UserId, + ProjectId: projectResp.Id, + RoleKeys: []string{role1, role2}, + OrganizationId: orgResp.OrganizationId, + }) + require.NoError(t, err) + afterCreate := time.Now().Add(500 * time.Millisecond) + + // ensure authorization exists + require.EventuallyWithT(t, func(collect *assert.CollectT) { + az, err := authorizationRepo.Get(CTX, pool, database.WithCondition( + authorizationRepo.PrimaryKeyCondition(instance.ID(), createdAuthorization.Id), + )) + require.NoError(collect, err) + assert.Equal(collect, []string{role1, role2}, az.Roles) + assert.Equal(collect, domain.AuthorizationStateActive, az.State) + assert.WithinRange(collect, az.CreatedAt, beforeCreate, afterCreate) + assert.WithinRange(collect, az.UpdatedAt, beforeCreate, afterCreate) + }, retryDuration, tick, "authorization not found within %v: %v", retryDuration, err) + + // delete the instance + _, err = Instance.Client.InstanceV2.DeleteInstance(CTX, &instance_v2.DeleteInstanceRequest{ + InstanceId: instance.ID(), + }) + require.NoError(t, err) + + // ensure authorization is deleted + require.EventuallyWithT(t, func(collect *assert.CollectT) { + az, err := authorizationRepo.Get(CTX, pool, database.WithCondition( + authorizationRepo.PrimaryKeyCondition(instance.ID(), createdAuthorization.Id), + )) + require.Empty(collect, az) + require.ErrorIs(collect, err, new(database.NoRowFoundError)) + }, retryDuration, tick, "authorization not deleted within %v: %v", retryDuration, err) + }) +} + +func createAndEnsureAuthorization(t *testing.T, instanceID, orgID, userID, projectID string, roles []string, retryDuration time.Duration, tick time.Duration) *authorization_v2.CreateAuthorizationResponse { + // create authorization + beforeCreate := time.Now() + createdAuthorization, err := AuthorizationClient.CreateAuthorization(CTX, &authorization_v2.CreateAuthorizationRequest{ + UserId: userID, + ProjectId: projectID, + RoleKeys: roles, + OrganizationId: orgID, + }) + require.NoError(t, err) + afterCreate := time.Now().Add(500 * time.Millisecond) + // ensure authorization exists + authzRepo := repository.AuthorizationRepository() + require.EventuallyWithT(t, func(collect *assert.CollectT) { + az, err := authzRepo.Get(CTX, pool, database.WithCondition( + authzRepo.PrimaryKeyCondition(instanceID, createdAuthorization.Id), + )) + require.NoError(collect, err) + assert.Equal(collect, roles, az.Roles) + assert.Equal(collect, domain.AuthorizationStateActive, az.State) + assert.WithinRange(collect, az.CreatedAt, beforeCreate, afterCreate) + assert.WithinRange(collect, az.UpdatedAt, beforeCreate, afterCreate) + }, retryDuration, tick, "authorization not found within %v: %v", retryDuration, err) + return createdAuthorization +} + +func prepareProjectAndProjectRoles(t *testing.T, orgID string, roles []string) string { + project, err := ProjectClient.CreateProject(CTX, &project_v2beta.CreateProjectRequest{ + OrganizationId: orgID, + Name: integration.ProjectName(), + }) + require.NoError(t, err) + + if len(roles) == 0 { + return project.Id + } + + // Wait for the project to be created in the relational db before adding roles + projectRepo := repository.ProjectRepository() + retryDuration, tick := integration.WaitForAndTickWithMaxDuration(CTX, time.Minute) + require.EventuallyWithT(t, func(collect *assert.CollectT) { + _, err := projectRepo.Get(CTX, pool, database.WithCondition( + projectRepo.PrimaryKeyCondition(Instance.ID(), project.Id), + )) + require.NoError(collect, err) + }, retryDuration, tick, "project not found") + + for _, role := range roles { + _, err = ProjectClient.AddProjectRole(CTX, &project_v2beta.AddProjectRoleRequest{ + ProjectId: project.GetId(), + RoleKey: role, + DisplayName: "display", + }) + require.NoError(t, err) + } + return project.Id +} diff --git a/backend/v3/storage/database/events_testing/events_test.go b/backend/v3/storage/database/events_testing/events_test.go index c5aad90d0..45b4a1bb9 100644 --- a/backend/v3/storage/database/events_testing/events_test.go +++ b/backend/v3/storage/database/events_testing/events_test.go @@ -17,11 +17,13 @@ import ( "github.com/zitadel/zitadel/backend/v3/storage/database/dialect/postgres" "github.com/zitadel/zitadel/internal/integration" "github.com/zitadel/zitadel/pkg/grpc/admin" + "github.com/zitadel/zitadel/pkg/grpc/authorization/v2" v2beta "github.com/zitadel/zitadel/pkg/grpc/instance/v2beta" mgmt "github.com/zitadel/zitadel/pkg/grpc/management" v2beta_org "github.com/zitadel/zitadel/pkg/grpc/org/v2beta" v2beta_project "github.com/zitadel/zitadel/pkg/grpc/project/v2beta" "github.com/zitadel/zitadel/pkg/grpc/system" + user_v2 "github.com/zitadel/zitadel/pkg/grpc/user/v2" ) func getEnv(key, fallback string) string { @@ -42,15 +44,17 @@ var ConnString = fmt.Sprintf( ) var ( - dbPool *pgxpool.Pool - CTX context.Context - IAMCTX context.Context - Instance *integration.Instance - SystemClient system.SystemServiceClient - OrgClient v2beta_org.OrganizationServiceClient - ProjectClient v2beta_project.ProjectServiceClient - AdminClient admin.AdminServiceClient - MgmtClient mgmt.ManagementServiceClient + dbPool *pgxpool.Pool + CTX context.Context + IAMCTX context.Context + Instance *integration.Instance + SystemClient system.SystemServiceClient + OrgClient v2beta_org.OrganizationServiceClient + ProjectClient v2beta_project.ProjectServiceClient + AdminClient admin.AdminServiceClient + MgmtClient mgmt.ManagementServiceClient + UserClient user_v2.UserServiceClient + AuthorizationClient authorization.AuthorizationServiceClient ) var pool database.Pool @@ -69,6 +73,8 @@ func TestMain(m *testing.M) { ProjectClient = Instance.Client.Projectv2Beta AdminClient = Instance.Client.Admin MgmtClient = Instance.Client.Mgmt + UserClient = Instance.Client.UserV2 + AuthorizationClient = Instance.Client.AuthorizationV2 defer func() { _, err := Instance.Client.InstanceV2Beta.DeleteInstance(CTX, &v2beta.DeleteInstanceRequest{ diff --git a/backend/v3/storage/database/repository/authorization.go b/backend/v3/storage/database/repository/authorization.go new file mode 100644 index 000000000..e5aa92748 --- /dev/null +++ b/backend/v3/storage/database/repository/authorization.go @@ -0,0 +1,319 @@ +package repository + +import ( + "context" + "time" + + "github.com/zitadel/zitadel/backend/v3/domain" + "github.com/zitadel/zitadel/backend/v3/storage/database" +) + +var _ domain.AuthorizationRepository = (*authorization)(nil) + +type authorization struct{} + +func AuthorizationRepository() domain.AuthorizationRepository { + return new(authorization) +} + +func (a authorization) unqualifiedTableName() string { + return "authorizations" +} + +func (a authorization) qualifiedTableName() string { + return "zitadel." + a.unqualifiedTableName() +} + +func (a authorization) unqualifiedAuthorizationRolesTableName() string { + return "authorization_roles" +} + +func (a authorization) qualifiedAuthorizationRolesTableName() string { + return "zitadel." + a.unqualifiedAuthorizationRolesTableName() +} + +// ------------------------------------------------------------- +// repository +// ------------------------------------------------------------- + +const insertAuthorizationStmt = `INSERT INTO zitadel.authorizations ( + instance_id, id, user_id, grant_id, project_id, state +) +VALUES ($1, $2, $3, $4, $5, $6) +RETURNING created_at, updated_at` + +const insertAuthorizationWithRolesStmt = `WITH roles AS ( + INSERT INTO zitadel.authorization_roles + (instance_id, authorization_id, grant_id, project_id, role_key) + VALUES ($1, $2, $4, $5, unnest($7::text[])))` + insertAuthorizationStmt + +// Create implements [domain.AuthorizationRepository]. +func (a authorization) Create(ctx context.Context, client database.QueryExecutor, authorization *domain.Authorization) error { + builder := database.NewStatementBuilder(insertAuthorizationWithRolesStmt, + authorization.InstanceID, + authorization.ID, + authorization.UserID, + authorization.GrantID, + authorization.ProjectID, + authorization.State, + authorization.Roles, + ) + + if err := client.QueryRow( + ctx, + builder.String(), + builder.Args()...). + Scan( + &authorization.CreatedAt, + &authorization.UpdatedAt, + ); err != nil { + return err + } + return nil +} + +const queryAuthorizationStmt = `SELECT zitadel.authorizations.instance_id, + zitadel.authorizations.id, + zitadel.authorizations.user_id, + zitadel.authorizations.grant_id, + zitadel.authorizations.project_id, + zitadel.authorizations.state, + zitadel.authorizations.created_at, + zitadel.authorizations.updated_at, + ARRAY_AGG(authorization_roles.role_key) + FILTER (WHERE authorization_roles.authorization_id IS NOT NULL) AS roles +FROM zitadel.authorizations + LEFT JOIN zitadel.authorization_roles + ON zitadel.authorizations.instance_id = zitadel.authorization_roles.instance_id + AND zitadel.authorizations.id = zitadel.authorization_roles.authorization_id` + +// Get implements [domain.AuthorizationRepository]. +func (a authorization) Get(ctx context.Context, client database.QueryExecutor, opts ...database.QueryOption) (*domain.Authorization, error) { + opts = append(opts, + database.WithGroupBy(a.InstanceIDColumn(), a.IDColumn()), + ) + + builder, err := a.prepareQuery(opts) + if err != nil { + return nil, err + } + return getOne[domain.Authorization](ctx, client, builder) +} + +// List implements [domain.AuthorizationRepository]. +func (a authorization) List(ctx context.Context, client database.QueryExecutor, opts ...database.QueryOption) ([]*domain.Authorization, error) { + opts = append(opts, + database.WithGroupBy(a.InstanceIDColumn(), a.IDColumn()), + ) + + builder, err := a.prepareQuery(opts) + if err != nil { + return nil, err + } + return getMany[domain.Authorization](ctx, client, builder) +} + +func (a authorization) prepareQuery(opts []database.QueryOption) (*database.StatementBuilder, error) { + options := new(database.QueryOpts) + for _, opt := range opts { + opt(options) + } + if err := checkRestrictingColumns(options.Condition, a.InstanceIDColumn()); err != nil { + return nil, err + } + builder := database.NewStatementBuilder(queryAuthorizationStmt) + options.Write(builder) + + return builder, nil +} + +const queryUpdateAuthorizationRoleStmt = `SELECT instance_id, id, project_id, grant_id, $1::text[] as roles from zitadel.authorizations` + +const updateAuthorizationRoleStmt = `deleted_roles AS ( + DELETE FROM zitadel.authorization_roles as azr + USING az + WHERE azr.instance_id = az.instance_id + AND azr.authorization_id = az.id + AND NOT azr.role_key = ANY ($1::text[]) + RETURNING * +), inserted_roles AS ( + INSERT INTO zitadel.authorization_roles (instance_id, authorization_id, project_id, grant_id, role_key) + SELECT instance_id, + id, + project_id, + grant_id, + UNNEST(az.roles) AS role_key + FROM az + ON CONFLICT DO NOTHING + RETURNING * +) +UPDATE zitadel.authorizations SET ` + +const updateAuthorizationRoleStmtWhere = ` FROM az +WHERE az.instance_id = authorizations.instance_id + AND az.id = authorizations.id` + +// Update implements [domain.AuthorizationRepository]. +func (a authorization) Update(ctx context.Context, client database.QueryExecutor, condition database.Condition, roles []string, changes ...database.Change) (int64, error) { + if roles != nil { + return a.setRoles(ctx, client, condition, roles, changes...) + } + return updateOne(ctx, client, a, condition, changes...) +} + +// setRoles sets the roles of an authorization. +func (a authorization) setRoles(ctx context.Context, client database.QueryExecutor, condition database.Condition, roles []string, changes ...database.Change) (int64, error) { + if err := checkPKCondition(a, condition); err != nil { + return 0, err + } + if !database.Changes(changes).IsOnColumn(a.UpdatedAtColumn()) { + changes = append(changes, database.NewChange(a.UpdatedAtColumn(), database.NullInstruction)) + } + + // get the authorization to be updated + builder := database.NewStatementBuilder("WITH az AS (") + builder.WriteString(queryUpdateAuthorizationRoleStmt) + builder.AppendArg(roles) + writeCondition(builder, condition) + builder.WriteString(" ), ") + + // set the roles + builder.WriteString(updateAuthorizationRoleStmt) + err := database.Changes(changes).Write(builder) + if err != nil { + return 0, err + } + builder.WriteString(updateAuthorizationRoleStmtWhere) + + return client.Exec(ctx, builder.String(), builder.Args()...) +} + +// Delete implements [domain.AuthorizationRepository]. +func (a authorization) Delete(ctx context.Context, client database.QueryExecutor, condition database.Condition) (int64, error) { + if !condition.IsRestrictingColumn(a.InstanceIDColumn()) { + return 0, database.NewMissingConditionError(a.InstanceIDColumn()) + } + + builder := database.NewStatementBuilder("DELETE FROM zitadel.authorizations") + writeCondition(builder, condition) + + return client.Exec(ctx, builder.String(), builder.Args()...) +} + +// ------------------------------------------------------------- +// columns +// ------------------------------------------------------------- + +// PrimaryKeyColumns implements [domain.Repository] +func (a authorization) PrimaryKeyColumns() []database.Column { + return []database.Column{ + a.InstanceIDColumn(), + a.IDColumn(), + } +} + +func (a authorization) IDColumn() database.Column { + return database.NewColumn(a.unqualifiedTableName(), "id") +} + +func (a authorization) UserIDColumn() database.Column { + return database.NewColumn(a.unqualifiedTableName(), "user_id") +} + +func (a authorization) GrantIDColumn() database.Column { + return database.NewColumn(a.unqualifiedTableName(), "grant_id") +} + +func (a authorization) ProjectIDColumn() database.Column { + return database.NewColumn(a.unqualifiedTableName(), "project_id") +} + +func (a authorization) InstanceIDColumn() database.Column { + return database.NewColumn(a.unqualifiedTableName(), "instance_id") +} + +func (a authorization) StateColumn() database.Column { + return database.NewColumn(a.unqualifiedTableName(), "state") +} + +func (a authorization) CreatedAtColumn() database.Column { + return database.NewColumn(a.unqualifiedTableName(), "created_at") +} + +func (a authorization) UpdatedAtColumn() database.Column { + return database.NewColumn(a.unqualifiedTableName(), "updated_at") +} + +// ------------------------------------------------------------- +// conditions +// ------------------------------------------------------------- + +// PrimaryKeyCondition implements [domain.authorizationConditions] +func (a authorization) PrimaryKeyCondition(instanceID, id string) database.Condition { + return database.And( + a.InstanceIDCondition(instanceID), + a.IDCondition(id), + ) +} + +// IDCondition implements [domain.authorizationConditions] +func (a authorization) IDCondition(id string) database.Condition { + return database.NewTextCondition(a.IDColumn(), database.TextOperationEqual, id) +} + +// InstanceIDCondition implements [domain.authorizationConditions] +func (a authorization) InstanceIDCondition(instanceID string) database.Condition { + return database.NewTextCondition(a.InstanceIDColumn(), database.TextOperationEqual, instanceID) +} + +// ProjectIDCondition implements [domain.authorizationConditions] +func (a authorization) ProjectIDCondition(projectID string) database.Condition { + return database.NewTextCondition(a.ProjectIDColumn(), database.TextOperationEqual, projectID) +} + +// GrantIDCondition implements [domain.authorizationConditions] +func (a authorization) GrantIDCondition(grantID string) database.Condition { + return database.NewTextCondition(a.GrantIDColumn(), database.TextOperationEqual, grantID) +} + +// UserIDCondition implements [domain.authorizationConditions] +func (a authorization) UserIDCondition(userID string) database.Condition { + return database.NewTextCondition(a.UserIDColumn(), database.TextOperationEqual, userID) +} + +// RoleCondition implements [domain.authorizationConditions] +func (a authorization) RoleCondition(op database.TextOperation, role string) database.Condition { + return database.NewTextCondition(database.NewColumn(a.unqualifiedAuthorizationRolesTableName(), "role_key"), op, role) +} + +// ExistsRole implements [domain.authorizationConditions] +// ExistsRole creates a correlated [database.Exists] condition on the authorization_roles table. +// Use this filter to make sure the authorization returned contains a specific role. +func (a authorization) ExistsRole(cond database.Condition) database.Condition { + return database.Exists( + a.qualifiedAuthorizationRolesTableName(), + database.And( + database.NewColumnCondition(a.InstanceIDColumn(), database.NewColumn(a.unqualifiedAuthorizationRolesTableName(), "instance_id")), + database.NewColumnCondition(a.IDColumn(), database.NewColumn(a.unqualifiedAuthorizationRolesTableName(), "authorization_id")), + cond, + ), + ) +} + +// StateCondition implements [domain.authorizationConditions] +func (a authorization) StateCondition(state domain.AuthorizationState) database.Condition { + return database.NewTextCondition(a.StateColumn(), database.TextOperationEqual, state.String()) +} + +// ------------------------------------------------------------- +// changes +// ------------------------------------------------------------- + +// SetState implements [domain.authorizationChanges] +func (a authorization) SetState(state domain.AuthorizationState) database.Change { + return database.NewChange(a.StateColumn(), state) +} + +func (a authorization) SetUpdatedAt(updatedAt time.Time) database.Change { + return database.NewChange(a.UpdatedAtColumn(), updatedAt) +} diff --git a/backend/v3/storage/database/repository/authorization_test.go b/backend/v3/storage/database/repository/authorization_test.go new file mode 100644 index 000000000..bacef1b3a --- /dev/null +++ b/backend/v3/storage/database/repository/authorization_test.go @@ -0,0 +1,1199 @@ +package repository_test + +import ( + "testing" + "time" + + "github.com/muhlemmer/gu" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + + "github.com/zitadel/zitadel/backend/v3/domain" + "github.com/zitadel/zitadel/backend/v3/storage/database" + "github.com/zitadel/zitadel/backend/v3/storage/database/repository" + "github.com/zitadel/zitadel/internal/integration" +) + +func TestCreateAuthorization(t *testing.T) { + beforeCreate := time.Now() + + tx, rollback := transactionForRollback(t) + defer rollback() + + instanceID := createInstance(t, tx) + organizationID := createOrganization(t, tx, instanceID) + // create a project + projectID := createProject(t, tx, instanceID, organizationID) + require.NotNil(t, projectID) + + // create project roles + role1 := createProjectRole(t, tx, instanceID, organizationID, projectID, "role1") + role2 := createProjectRole(t, tx, instanceID, organizationID, projectID, "role2") + // TODO: uncomment when user table is available + // userID := createUser(t, tx, instanceID, organizationID) + // require.NotNil(t, userID) + userID := integration.ID() + + authorizationRepo := repository.AuthorizationRepository() + + // create project authorization + existingAuthorization := &domain.Authorization{ + ID: integration.ID(), + UserID: userID, + ProjectID: projectID, + InstanceID: instanceID, + State: domain.AuthorizationStateActive, + Roles: []string{role1}, + } + err := authorizationRepo.Create(t.Context(), tx, existingAuthorization) + require.NoError(t, err) + + // create a project grant + grantedOrganizationID := createOrganization(t, tx, instanceID) + grantID := createProjectGrant(t, tx, instanceID, organizationID, grantedOrganizationID, projectID, []string{role1}) + + tests := []struct { + name string + authorization *domain.Authorization + wantErr error + }{ + { + name: "non-existent instance", + authorization: &domain.Authorization{ + ID: integration.ID(), + UserID: userID, + ProjectID: projectID, + InstanceID: "random-id", + State: domain.AuthorizationStateActive, + Roles: nil, + }, + wantErr: new(database.ForeignKeyError), + }, + { + name: "non-existent project", + authorization: &domain.Authorization{ + ID: integration.ID(), + UserID: userID, + ProjectID: "random-id", + InstanceID: instanceID, + State: domain.AuthorizationStateActive, + Roles: nil, + }, + wantErr: new(database.ForeignKeyError), + }, + { + name: "non-existent project role", + authorization: &domain.Authorization{ + ID: integration.ID(), + UserID: userID, + ProjectID: "random-id", + InstanceID: instanceID, + State: domain.AuthorizationStateActive, + Roles: []string{"role3"}, + }, + wantErr: new(database.ForeignKeyError), + }, + { + name: "non-existent project grant", + authorization: &domain.Authorization{ + ID: integration.ID(), + UserID: userID, + ProjectID: projectID, + GrantID: gu.Ptr("random-id"), + InstanceID: instanceID, + State: domain.AuthorizationStateActive, + Roles: nil, + }, + wantErr: new(database.ForeignKeyError), + }, + // TODO: uncomment when user table is available + // { + // name: "non-existent user", + // authorization: &domain.Authorization{ + // ID: integration.ID(), + // UserID: "random-id", + // ProjectID: projectID, + // InstanceID: instanceID, + // State: domain.AuthorizationStateActive, + // Roles: nil, + // }, + // wantErr: new(database.ForeignKeyError), + // }, + { + name: "duplicate authorization", + authorization: &domain.Authorization{ + ID: existingAuthorization.ID, + UserID: existingAuthorization.UserID, + ProjectID: existingAuthorization.ProjectID, + GrantID: existingAuthorization.GrantID, + InstanceID: existingAuthorization.InstanceID, + State: domain.AuthorizationStateActive, + Roles: nil, + }, + wantErr: database.NewUniqueError("authorizations", "uq_authorizations_instance_id_id", nil), + }, + { + name: "missing ID", + authorization: &domain.Authorization{ + UserID: userID, + ProjectID: projectID, + InstanceID: instanceID, + State: domain.AuthorizationStateActive, + Roles: nil, + }, + wantErr: database.NewCheckError("authorizations", "", nil), + }, + { + name: "create authorization without roles", + authorization: &domain.Authorization{ + ID: integration.ID(), + UserID: userID, + ProjectID: projectID, + InstanceID: instanceID, + State: domain.AuthorizationStateActive, + Roles: nil, + }, + }, + { + name: "create authorization with roles", + authorization: &domain.Authorization{ + ID: integration.ID(), + UserID: userID, + ProjectID: projectID, + InstanceID: instanceID, + State: domain.AuthorizationStateActive, + Roles: []string{role1, role2}, + }, + }, + { + name: "create authorization for project grant with valid role", + authorization: &domain.Authorization{ + ID: integration.ID(), + UserID: userID, + ProjectID: projectID, + GrantID: gu.Ptr(grantID), + InstanceID: instanceID, + State: domain.AuthorizationStateActive, + Roles: []string{role1}, + }, + }, + { + name: "create authorization for project grant with unassigned role", + authorization: &domain.Authorization{ + ID: integration.ID(), + UserID: userID, + ProjectID: projectID, + GrantID: gu.Ptr(grantID), + InstanceID: instanceID, + State: domain.AuthorizationStateActive, + Roles: []string{role2}, + }, + wantErr: new(database.ForeignKeyError), + }, + { + name: "create authorization for project grant without roles", + authorization: &domain.Authorization{ + ID: integration.ID(), + UserID: userID, + ProjectID: projectID, + GrantID: gu.Ptr(grantID), + InstanceID: instanceID, + State: domain.AuthorizationStateActive, + Roles: nil, + }, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + savepoint, rollback := savepointForRollback(t, tx) + defer rollback() + + err := authorizationRepo.Create(t.Context(), savepoint, tt.authorization) + require.ErrorIs(t, err, tt.wantErr) + if err != nil { + return + } + afterCreate := time.Now() + + // get authorization + got, err := authorizationRepo.Get(t.Context(), savepoint, + database.WithCondition( + authorizationRepo.PrimaryKeyCondition(tt.authorization.InstanceID, tt.authorization.ID), + ), + ) + require.NoError(t, err) + require.Equal(t, tt.authorization, got) + assert.WithinRange(t, got.CreatedAt, beforeCreate, afterCreate) + assert.WithinRange(t, got.UpdatedAt, beforeCreate, afterCreate) + }) + } +} + +func TestGetAuthorization(t *testing.T) { + tx, rollback := transactionForRollback(t) + defer rollback() + + instanceID := createInstance(t, tx) + organizationID := createOrganization(t, tx, instanceID) + + projectID := createProject(t, tx, instanceID, organizationID) + require.NotNil(t, projectID) + role1 := createProjectRole(t, tx, instanceID, organizationID, projectID, "role1") + role2 := createProjectRole(t, tx, instanceID, organizationID, projectID, "role2") + + // TODO: uncomment when user table is available + // userID := createUser(t, tx, instanceID, organizationID) + // require.NotNil(t, userID) + userID := integration.ID() + + // create authorization with roles + authorizationRepo := repository.AuthorizationRepository() + authorizationWithRolesUser := &domain.Authorization{ + ID: integration.ID(), + UserID: userID, + ProjectID: projectID, + InstanceID: instanceID, + State: domain.AuthorizationStateActive, + Roles: []string{role1, role2}, + } + err := authorizationRepo.Create(t.Context(), tx, authorizationWithRolesUser) + require.NoError(t, err) + + // create authorization without roles + authorizationWithoutRolesUser := &domain.Authorization{ + ID: integration.ID(), + UserID: userID, + ProjectID: projectID, + InstanceID: instanceID, + State: domain.AuthorizationStateActive, + Roles: nil, + } + err = authorizationRepo.Create(t.Context(), tx, authorizationWithoutRolesUser) + require.NoError(t, err) + + // create authorization for a project grant with/without roles + grantedOrganizationID := createOrganization(t, tx, instanceID) + grantID := createProjectGrant(t, tx, instanceID, organizationID, grantedOrganizationID, projectID, []string{role1}) + + authorizationProjectGrantWithRoles := &domain.Authorization{ + ID: integration.ID(), + UserID: userID, + ProjectID: projectID, + GrantID: gu.Ptr(grantID), + InstanceID: instanceID, + State: domain.AuthorizationStateActive, + Roles: []string{role1}, + } + err = authorizationRepo.Create(t.Context(), tx, authorizationProjectGrantWithRoles) + require.NoError(t, err) + + authorizationProjectGrantWithoutRoles := &domain.Authorization{ + ID: integration.ID(), + UserID: userID, + ProjectID: projectID, + GrantID: gu.Ptr(grantID), + InstanceID: instanceID, + State: domain.AuthorizationStateActive, + } + err = authorizationRepo.Create(t.Context(), tx, authorizationProjectGrantWithoutRoles) + require.NoError(t, err) + + tests := []struct { + name string + condition database.Condition + want *domain.Authorization + wantErr error + }{ + { + name: "incomplete condition", + condition: authorizationRepo.IDCondition("123"), + wantErr: new(database.MissingConditionError), + }, + { + name: "non-existent authorization", + condition: authorizationRepo.PrimaryKeyCondition( + instanceID, + "random-id", + ), + wantErr: new(database.NoRowFoundError), + }, + { + name: "get authorization with roles", + condition: authorizationRepo.PrimaryKeyCondition( + authorizationWithRolesUser.InstanceID, + authorizationWithRolesUser.ID, + ), + want: authorizationWithRolesUser, + }, + { + name: "get authorization without roles", + condition: authorizationRepo.PrimaryKeyCondition( + authorizationWithoutRolesUser.InstanceID, + authorizationWithoutRolesUser.ID, + ), + want: authorizationWithoutRolesUser, + }, + { + name: "get authorization, multiple rows, error", + condition: database.And( + authorizationRepo.InstanceIDCondition(authorizationWithRolesUser.InstanceID), + ), + wantErr: new(database.MultipleRowsFoundError), + }, + { + name: "get authorization with role condition", + condition: database.And(authorizationRepo.PrimaryKeyCondition( + authorizationWithRolesUser.InstanceID, + authorizationWithRolesUser.ID, + ), + authorizationRepo.RoleCondition(database.TextOperationEqual, role1)), + want: &domain.Authorization{ + ID: authorizationWithRolesUser.ID, + UserID: userID, + ProjectID: projectID, + InstanceID: instanceID, + State: domain.AuthorizationStateActive, + Roles: []string{role1}, + CreatedAt: authorizationWithRolesUser.CreatedAt, + UpdatedAt: authorizationWithRolesUser.UpdatedAt, + }, + }, + { + name: "get authorization with non-existent role, error", + condition: database.And(authorizationRepo.PrimaryKeyCondition( + authorizationWithRolesUser.InstanceID, + authorizationWithRolesUser.ID, + ), + authorizationRepo.RoleCondition(database.TextOperationEqual, "random-role")), + wantErr: new(database.NoRowFoundError), + }, + { + name: "get authorization with grant ID condition (with roles)", + condition: database.And(authorizationRepo.PrimaryKeyCondition( + authorizationProjectGrantWithRoles.InstanceID, + authorizationProjectGrantWithRoles.ID, + ), + authorizationRepo.GrantIDCondition(grantID)), + want: authorizationProjectGrantWithRoles, + }, + { + name: "get authorization with grant ID condition (without roles)", + condition: database.And(authorizationRepo.PrimaryKeyCondition( + authorizationProjectGrantWithoutRoles.InstanceID, + authorizationProjectGrantWithoutRoles.ID, + ), + authorizationRepo.GrantIDCondition(grantID)), + want: authorizationProjectGrantWithoutRoles, + }, + { + name: "get authorization for project grant with non-existent grant ID, error", + condition: database.And(authorizationRepo.PrimaryKeyCondition( + authorizationProjectGrantWithoutRoles.InstanceID, + authorizationProjectGrantWithoutRoles.ID, + ), + authorizationRepo.GrantIDCondition("random-id")), + wantErr: new(database.NoRowFoundError), + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got, err := authorizationRepo.Get(t.Context(), tx, + database.WithCondition(tt.condition), + ) + require.ErrorIs(t, err, tt.wantErr) + if err != nil { + return + } + require.Equal(t, tt.want, got) + }) + } +} + +func TestListAuthorization(t *testing.T) { + tx, rollback := transactionForRollback(t) + defer rollback() + + instanceID := createInstance(t, tx) + organizationID := createOrganization(t, tx, instanceID) + + project1ID := createProject(t, tx, instanceID, organizationID) + require.NotNil(t, project1ID) + project1Role1 := createProjectRole(t, tx, instanceID, organizationID, project1ID, "project1Role1") + project1Role2 := createProjectRole(t, tx, instanceID, organizationID, project1ID, "project1Role2") + + project2ID := createProject(t, tx, instanceID, organizationID) + require.NotNil(t, project2ID) + project2Role1 := createProjectRole(t, tx, instanceID, organizationID, project2ID, "project2Role1") + project2Role2 := createProjectRole(t, tx, instanceID, organizationID, project2ID, "project2Role2") + + // TODO: uncomment when user table is available + // user1ID := createUser(t, tx, instanceID, organizationID) + // require.NotNil(t, user1ID) + user1ID := integration.ID() + // user2ID := createUser(t, tx, instanceID, organizationID) + // require.NotNil(t, user2ID) + user2ID := integration.ID() + + // create authorization with roles for user1 for project1 + authorizationRepo := repository.AuthorizationRepository() + authorizationWithRolesUser1Project1 := &domain.Authorization{ + ID: "authorization1-user1", + UserID: user1ID, + ProjectID: project1ID, + InstanceID: instanceID, + State: domain.AuthorizationStateActive, + Roles: []string{project1Role1, project1Role2}, + } + err := authorizationRepo.Create(t.Context(), tx, authorizationWithRolesUser1Project1) + require.NoError(t, err) + + // create authorization without roles for user1 for project1 + authorizationWithoutRolesUser1Project1 := &domain.Authorization{ + ID: "authorization2-user1", + UserID: user1ID, + ProjectID: project1ID, + InstanceID: instanceID, + State: domain.AuthorizationStateActive, + Roles: nil, + } + err = authorizationRepo.Create(t.Context(), tx, authorizationWithoutRolesUser1Project1) + require.NoError(t, err) + + // create authorization with roles for user1 for project2 + authorizationWithRolesUser1Project2 := &domain.Authorization{ + ID: "authorization3-user1", + UserID: user1ID, + ProjectID: project2ID, + InstanceID: instanceID, + State: domain.AuthorizationStateActive, + Roles: []string{project2Role1, project2Role2}, + } + err = authorizationRepo.Create(t.Context(), tx, authorizationWithRolesUser1Project2) + require.NoError(t, err) + + // create authorization without roles for user1 for project2 + authorizationWithoutRolesUser1Project2 := &domain.Authorization{ + ID: "authorization4-user1", + UserID: user1ID, + ProjectID: project2ID, + + InstanceID: instanceID, + State: domain.AuthorizationStateInactive, + Roles: nil, + } + err = authorizationRepo.Create(t.Context(), tx, authorizationWithoutRolesUser1Project2) + require.NoError(t, err) + + // create authorization with roles for user2 for project1 + authorizationWithRolesUser2Project1 := &domain.Authorization{ + ID: "authorization5-user2", + UserID: user2ID, + ProjectID: project1ID, + + InstanceID: instanceID, + State: domain.AuthorizationStateActive, + Roles: []string{project1Role1, project1Role2}, + } + err = authorizationRepo.Create(t.Context(), tx, authorizationWithRolesUser2Project1) + require.NoError(t, err) + + // create authorization without roles for user2 for project1 + authorizationWithoutRolesUser2Project1 := &domain.Authorization{ + ID: "authorization6-user2", + UserID: user2ID, + ProjectID: project1ID, + InstanceID: instanceID, + State: domain.AuthorizationStateInactive, + Roles: nil, + } + err = authorizationRepo.Create(t.Context(), tx, authorizationWithoutRolesUser2Project1) + require.NoError(t, err) + + // create authorization with roles for user2 for project2 + authorizationWithRolesUser2Project2 := &domain.Authorization{ + ID: "authorization7-user2", + UserID: user2ID, + ProjectID: project2ID, + InstanceID: instanceID, + State: domain.AuthorizationStateActive, + Roles: []string{project2Role1, project2Role2}, + } + err = authorizationRepo.Create(t.Context(), tx, authorizationWithRolesUser2Project2) + require.NoError(t, err) + + // create authorization without roles for user2 for project2 + authorizationWithoutRolesUser2Project2 := &domain.Authorization{ + ID: "authorization8-user2", + UserID: user2ID, + ProjectID: project2ID, + InstanceID: instanceID, + State: domain.AuthorizationStateActive, + Roles: nil, + } + err = authorizationRepo.Create(t.Context(), tx, authorizationWithoutRolesUser2Project2) + require.NoError(t, err) + + // create a project grant + grantedOrganizationID := createOrganization(t, tx, instanceID) + grantID := createProjectGrant(t, tx, instanceID, organizationID, grantedOrganizationID, project1ID, []string{project1Role1, project1Role2}) + + authorizationProjectGrantWithRoles := &domain.Authorization{ + ID: "authorization9-user1", + UserID: user1ID, + ProjectID: project1ID, + GrantID: gu.Ptr(grantID), + InstanceID: instanceID, + State: domain.AuthorizationStateActive, + Roles: []string{project1Role1}, + } + err = authorizationRepo.Create(t.Context(), tx, authorizationProjectGrantWithRoles) + require.NoError(t, err) + + authorizationProjectGrantWithoutRoles := &domain.Authorization{ + ID: "authorization10-user2", + UserID: user2ID, + ProjectID: project1ID, + GrantID: gu.Ptr(grantID), + InstanceID: instanceID, + State: domain.AuthorizationStateActive, + } + err = authorizationRepo.Create(t.Context(), tx, authorizationProjectGrantWithoutRoles) + require.NoError(t, err) + + tests := []struct { + name string + condition []database.QueryOption + want []*domain.Authorization + wantErr error + }{ + { + name: "incomplete condition", + condition: []database.QueryOption{ + database.WithCondition( + authorizationRepo.IDCondition("123"), + ), + }, + wantErr: new(database.MissingConditionError), + }, + { + name: "non-existent authorization", + condition: []database.QueryOption{ + database.WithCondition(authorizationRepo.PrimaryKeyCondition( + instanceID, + "1234"), + ), + }, + want: []*domain.Authorization{}, + }, + { + name: "list all authorizations in instance", + condition: []database.QueryOption{ + database.WithCondition(authorizationRepo.InstanceIDCondition( + instanceID, + )), + database.WithOrderByAscending(authorizationRepo.IDColumn()), + }, + want: []*domain.Authorization{ + authorizationWithRolesUser1Project1, + authorizationProjectGrantWithoutRoles, + authorizationWithoutRolesUser1Project1, + authorizationWithRolesUser1Project2, + authorizationWithoutRolesUser1Project2, + authorizationWithRolesUser2Project1, + authorizationWithoutRolesUser2Project1, + authorizationWithRolesUser2Project2, + authorizationWithoutRolesUser2Project2, + authorizationProjectGrantWithRoles, + }, + }, + { + name: "list all authorizations for user1", + condition: []database.QueryOption{ + database.WithCondition( + database.And( + authorizationRepo.InstanceIDCondition(instanceID), + authorizationRepo.UserIDCondition(user1ID), + ), + ), + database.WithOrderByAscending(authorizationRepo.IDColumn()), + }, + want: []*domain.Authorization{ + authorizationWithRolesUser1Project1, + authorizationWithoutRolesUser1Project1, + authorizationWithRolesUser1Project2, + authorizationWithoutRolesUser1Project2, + authorizationProjectGrantWithRoles, + }, + }, + { + name: "list all authorizations for user2", + condition: []database.QueryOption{ + database.WithCondition( + database.And( + authorizationRepo.InstanceIDCondition(instanceID), + authorizationRepo.UserIDCondition(user2ID), + ), + ), + }, + want: []*domain.Authorization{ + authorizationProjectGrantWithoutRoles, + authorizationWithRolesUser2Project1, + authorizationWithoutRolesUser2Project1, + authorizationWithRolesUser2Project2, + authorizationWithoutRolesUser2Project2, + }, + }, + { + name: "list all authorizations for user1 and project2", + condition: []database.QueryOption{ + database.WithCondition( + database.And( + authorizationRepo.InstanceIDCondition(instanceID), + authorizationRepo.UserIDCondition(user1ID), + authorizationRepo.ProjectIDCondition(project2ID), + ), + ), + }, + want: []*domain.Authorization{ + authorizationWithRolesUser1Project2, + authorizationWithoutRolesUser1Project2, + }, + }, + { + name: "list all authorizations for user2 and project1, ordered by ID descending", + condition: []database.QueryOption{ + database.WithCondition( + database.And( + authorizationRepo.InstanceIDCondition(instanceID), + authorizationRepo.UserIDCondition(user2ID), + authorizationRepo.ProjectIDCondition(project1ID), + ), + ), + database.WithOrderByDescending(authorizationRepo.IDColumn()), + }, + want: []*domain.Authorization{ + authorizationWithoutRolesUser2Project1, + authorizationWithRolesUser2Project1, + authorizationProjectGrantWithoutRoles, + }, + }, + { + name: "list all authorizations for project1, limited to 2", + condition: []database.QueryOption{ + database.WithCondition( + database.And( + authorizationRepo.InstanceIDCondition(instanceID), + authorizationRepo.ProjectIDCondition(project1ID), + ), + ), + database.WithLimit(2), + database.WithOrderByDescending(authorizationRepo.IDColumn()), + }, + want: []*domain.Authorization{ + authorizationProjectGrantWithRoles, + authorizationWithoutRolesUser2Project1, + }, + }, + { + name: "list all inactive authorizations", + condition: []database.QueryOption{ + database.WithCondition( + database.And( + authorizationRepo.InstanceIDCondition(instanceID), + authorizationRepo.StateCondition(domain.AuthorizationStateInactive), + ), + ), + database.WithOrderByAscending(authorizationRepo.IDColumn()), + }, + want: []*domain.Authorization{ + authorizationWithoutRolesUser1Project2, + authorizationWithoutRolesUser2Project1, + }, + }, + { + name: "list all authorizations with role project1Role1", + condition: []database.QueryOption{ + database.WithCondition( + database.And( + authorizationRepo.InstanceIDCondition(instanceID), + authorizationRepo.RoleCondition(database.TextOperationContains, project1Role1), + ), + ), + database.WithOrderByAscending(authorizationRepo.IDColumn()), + }, + want: []*domain.Authorization{ + { + ID: "authorization1-user1", + UserID: user1ID, + ProjectID: project1ID, + GrantID: authorizationWithRolesUser1Project1.GrantID, + InstanceID: instanceID, + State: domain.AuthorizationStateActive, + Roles: []string{project1Role1}, + CreatedAt: authorizationWithRolesUser1Project1.CreatedAt, + UpdatedAt: authorizationWithRolesUser1Project1.UpdatedAt, + }, + { + ID: "authorization5-user2", + UserID: user2ID, + ProjectID: project1ID, + GrantID: authorizationWithRolesUser2Project1.GrantID, + InstanceID: instanceID, + State: domain.AuthorizationStateActive, + Roles: []string{project1Role1}, + CreatedAt: authorizationWithRolesUser2Project1.CreatedAt, + UpdatedAt: authorizationWithRolesUser2Project1.UpdatedAt, + }, + { + ID: "authorization9-user1", + UserID: user1ID, + ProjectID: project1ID, + GrantID: gu.Ptr(grantID), + InstanceID: instanceID, + State: domain.AuthorizationStateActive, + Roles: []string{project1Role1}, + CreatedAt: authorizationProjectGrantWithRoles.CreatedAt, + UpdatedAt: authorizationProjectGrantWithRoles.UpdatedAt, + }, + }, + }, + { + name: "list all authorizations for project grant", + condition: []database.QueryOption{ + database.WithCondition( + database.And( + authorizationRepo.InstanceIDCondition(instanceID), + authorizationRepo.GrantIDCondition(grantID), + ), + ), + database.WithOrderByDescending(authorizationRepo.IDColumn()), + }, + want: []*domain.Authorization{ + authorizationProjectGrantWithRoles, + authorizationProjectGrantWithoutRoles, + }, + }, + { + name: "list all authorizations for non-existent project grant", + condition: []database.QueryOption{ + database.WithCondition( + database.And( + authorizationRepo.InstanceIDCondition(instanceID), + authorizationRepo.GrantIDCondition("random-id"), + ), + ), + }, + want: []*domain.Authorization{}, + }, + { + name: "list all authorizations with exists role condition", + condition: []database.QueryOption{ + database.WithCondition( + database.And( + authorizationRepo.InstanceIDCondition(instanceID), + authorizationRepo.ExistsRole(authorizationRepo.RoleCondition(database.TextOperationEqual, project2Role2)), + ), + ), + database.WithOrderByAscending(authorizationRepo.IDColumn()), + }, + want: []*domain.Authorization{ + authorizationWithRolesUser1Project2, + authorizationWithRolesUser2Project2, + }, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got, err := authorizationRepo.List(t.Context(), tx, tt.condition...) + require.ErrorIs(t, err, tt.wantErr) + if err != nil { + return + } + require.Len(t, got, len(tt.want)) + for i := range got { + assertAuthorization(t, tt.want[i], got[i]) + } + }) + } +} + +func assertAuthorization(t *testing.T, expected, actual *domain.Authorization) { + assert.Equal(t, expected.ID, actual.ID) + assert.Equal(t, expected.UserID, actual.UserID) + assert.Equal(t, expected.ProjectID, actual.ProjectID) + assert.Equal(t, expected.GrantID, actual.GrantID) + assert.Equal(t, expected.InstanceID, actual.InstanceID) + assert.Equal(t, expected.State, actual.State) + assert.ElementsMatch(t, expected.Roles, actual.Roles) + assert.Equal(t, expected.CreatedAt, actual.CreatedAt) + assert.Equal(t, expected.UpdatedAt, actual.UpdatedAt) +} + +func TestUpdateAuthorization(t *testing.T) { + beforeUpdate := time.Now() + + tx, rollback := transactionForRollback(t) + defer rollback() + + instanceID := createInstance(t, tx) + organizationID := createOrganization(t, tx, instanceID) + projectID := createProject(t, tx, instanceID, organizationID) + + authorizationRepo := repository.AuthorizationRepository() + activeAuthorization := &domain.Authorization{ + ID: integration.ID(), + UserID: integration.ID(), + ProjectID: projectID, + InstanceID: instanceID, + State: domain.AuthorizationStateActive, + Roles: nil, + } + err := authorizationRepo.Create(t.Context(), tx, activeAuthorization) + require.NoError(t, err) + + inactiveAuthorization := &domain.Authorization{ + ID: integration.ID(), + UserID: integration.ID(), + ProjectID: projectID, + InstanceID: instanceID, + State: domain.AuthorizationStateInactive, + Roles: nil, + } + err = authorizationRepo.Create(t.Context(), tx, inactiveAuthorization) + require.NoError(t, err) + + tests := []struct { + name string + condition database.Condition + changes []database.Change + wantState domain.AuthorizationState + wantRowsAffected int64 + wantErr error + }{ + { + name: "no changes", + condition: authorizationRepo.PrimaryKeyCondition( + instanceID, + activeAuthorization.ID, + ), + changes: nil, + wantErr: database.ErrNoChanges, + }, + { + name: "incomplete condition", + condition: authorizationRepo.IDCondition(activeAuthorization.ID), + changes: []database.Change{ + authorizationRepo.SetState(domain.AuthorizationStateInactive), + }, + wantErr: new(database.MissingConditionError), + }, + { + name: "update state to inactive", + condition: authorizationRepo.PrimaryKeyCondition( + instanceID, + activeAuthorization.ID, + ), + changes: []database.Change{ + authorizationRepo.SetState(domain.AuthorizationStateInactive), + }, + wantState: domain.AuthorizationStateInactive, + wantRowsAffected: 1, + }, + { + name: "update state to active", + condition: authorizationRepo.PrimaryKeyCondition( + instanceID, + inactiveAuthorization.ID, + ), + changes: []database.Change{ + authorizationRepo.SetState(domain.AuthorizationStateActive), + }, + wantState: domain.AuthorizationStateActive, + wantRowsAffected: 1, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + savepoint, rollback := savepointForRollback(t, tx) + defer rollback() + + rowsAffected, err := authorizationRepo.Update(t.Context(), savepoint, tt.condition, nil, tt.changes...) + require.ErrorIs(t, err, tt.wantErr) + if err != nil { + return + } + afterUpdate := time.Now() + assert.Equal(t, tt.wantRowsAffected, rowsAffected) + + if tt.wantRowsAffected == 0 { + return + } + // verify update + updatedAuthorization, err := authorizationRepo.Get(t.Context(), savepoint, database.WithCondition(tt.condition)) + require.NoError(t, err) + assert.Equal(t, tt.wantState, updatedAuthorization.State) + assert.WithinRange(t, updatedAuthorization.UpdatedAt, beforeUpdate, afterUpdate) + }) + } + +} + +func TestAuthorizationUpdate_setRoles(t *testing.T) { + beforeUpdate := time.Now() + + tx, rollback := transactionForRollback(t) + defer rollback() + + instanceID := createInstance(t, tx) + organizationID := createOrganization(t, tx, instanceID) + projectID := createProject(t, tx, instanceID, organizationID) + role1 := createProjectRole(t, tx, instanceID, organizationID, projectID, "role1") + role2 := createProjectRole(t, tx, instanceID, organizationID, projectID, "role2") + role3 := createProjectRole(t, tx, instanceID, organizationID, projectID, "role3") + role4 := createProjectRole(t, tx, instanceID, organizationID, projectID, "role4") + + project2ID := createProject(t, tx, instanceID, organizationID) + role5 := createProjectRole(t, tx, instanceID, organizationID, project2ID, "role5") + role6 := createProjectRole(t, tx, instanceID, organizationID, project2ID, "role6") + + authorizationRepo := repository.AuthorizationRepository() + authorization1 := &domain.Authorization{ + ID: integration.ID(), + UserID: integration.ID(), + ProjectID: projectID, + InstanceID: instanceID, + State: domain.AuthorizationStateActive, + Roles: nil, + } + err := authorizationRepo.Create(t.Context(), tx, authorization1) + require.NoError(t, err) + + authorization2 := &domain.Authorization{ + ID: integration.ID(), + UserID: integration.ID(), + ProjectID: project2ID, + InstanceID: instanceID, + State: domain.AuthorizationStateActive, + Roles: []string{role5, role6}, + } + err = authorizationRepo.Create(t.Context(), tx, authorization2) + require.NoError(t, err) + + tests := []struct { + name string + roles []string + condition database.Condition + wantRowsAffected int64 + wantErr error + }{ + { + name: "incomplete primary key condition - missing instance ID", + roles: []string{role1, role2}, + condition: authorizationRepo.IDCondition(authorization1.ID), + wantErr: new(database.MissingConditionError), + }, + { + name: "incomplete primary key condition - missing authorization ID", + roles: []string{role1, role2}, + condition: authorizationRepo.InstanceIDCondition(authorization1.InstanceID), + wantErr: new(database.MissingConditionError), + }, + { + name: "non-existent role", + roles: []string{"random-role"}, + condition: authorizationRepo.PrimaryKeyCondition(authorization1.InstanceID, authorization1.ID), + wantErr: new(database.ForeignKeyError), + }, + { + name: "set single role", + roles: []string{role1}, + condition: authorizationRepo.PrimaryKeyCondition(authorization1.InstanceID, authorization1.ID), + wantRowsAffected: 1, + }, + { + name: "set multiple roles", + roles: []string{role1, role2}, + condition: authorizationRepo.PrimaryKeyCondition(authorization1.InstanceID, authorization1.ID), + wantRowsAffected: 1, + }, + { + name: "overwrite roles", + roles: []string{role3, role4}, + condition: authorizationRepo.PrimaryKeyCondition(authorization1.InstanceID, authorization1.ID), + wantRowsAffected: 1, + }, + { + name: "remove role", + roles: []string{role3}, + condition: authorizationRepo.PrimaryKeyCondition(authorization1.InstanceID, authorization1.ID), + wantRowsAffected: 1, + }, + { + name: "no changes", + roles: []string{role5, role6}, + condition: authorizationRepo.PrimaryKeyCondition(authorization2.InstanceID, authorization2.ID), + wantRowsAffected: 1, + }, + { + name: "clear roles", + roles: []string{}, + condition: authorizationRepo.PrimaryKeyCondition(authorization2.InstanceID, authorization2.ID), + wantRowsAffected: 1, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + savepoint, rollback := savepointForRollback(t, tx) + defer rollback() + + rowsAffected, err := authorizationRepo.Update(t.Context(), savepoint, tt.condition, tt.roles) + require.ErrorIs(t, err, tt.wantErr) + if err != nil { + return + } + afterUpdate := time.Now() + require.Equal(t, tt.wantRowsAffected, rowsAffected) + + // get roles + updatedAuthorization, err := authorizationRepo.Get(t.Context(), savepoint, database.WithCondition(tt.condition)) + require.NoError(t, err) + assert.ElementsMatch(t, tt.roles, updatedAuthorization.Roles) + assert.WithinRange(t, updatedAuthorization.UpdatedAt, beforeUpdate, afterUpdate) + }) + } +} + +func TestDeleteAuthorization(t *testing.T) { + tx, rollback := transactionForRollback(t) + defer rollback() + + instanceID := createInstance(t, tx) + organizationID := createOrganization(t, tx, instanceID) + projectID := createProject(t, tx, instanceID, organizationID) + role1 := createProjectRole(t, tx, instanceID, organizationID, projectID, "role1") + role2 := createProjectRole(t, tx, instanceID, organizationID, projectID, "role2") + + authorizationRepo := repository.AuthorizationRepository() + authorizationWithRoles := &domain.Authorization{ + ID: integration.ID(), + UserID: integration.ID(), + ProjectID: projectID, + InstanceID: instanceID, + State: domain.AuthorizationStateActive, + Roles: []string{role1, role2}, + } + err := authorizationRepo.Create(t.Context(), tx, authorizationWithRoles) + require.NoError(t, err) + + authorizationWithoutRoles := &domain.Authorization{ + ID: integration.ID(), + UserID: integration.ID(), + ProjectID: projectID, + InstanceID: instanceID, + State: domain.AuthorizationStateActive, + Roles: nil, + } + err = authorizationRepo.Create(t.Context(), tx, authorizationWithoutRoles) + require.NoError(t, err) + + deletedAuthorization := &domain.Authorization{ + ID: integration.ID(), + UserID: integration.ID(), + ProjectID: projectID, + InstanceID: instanceID, + State: domain.AuthorizationStateActive, + Roles: nil, + } + err = authorizationRepo.Create(t.Context(), tx, deletedAuthorization) + require.NoError(t, err) + _, err = authorizationRepo.Delete(t.Context(), tx, authorizationRepo.PrimaryKeyCondition( + instanceID, + deletedAuthorization.ID, + )) + require.NoError(t, err) + + grantedOrganizationID := createOrganization(t, tx, instanceID) + grantID := createProjectGrant(t, tx, instanceID, organizationID, grantedOrganizationID, projectID, []string{role1}) + authorizationProjectGrant := &domain.Authorization{ + ID: integration.ID(), + UserID: integration.ID(), + ProjectID: projectID, + GrantID: gu.Ptr(grantID), + InstanceID: instanceID, + State: domain.AuthorizationStateActive, + Roles: []string{role1}, + } + err = authorizationRepo.Create(t.Context(), tx, authorizationProjectGrant) + require.NoError(t, err) + + tests := []struct { + name string + condition database.Condition + wantRowsAffected int64 + wantErr error + }{ + { + name: "incomplete condition", + condition: authorizationRepo.IDCondition(authorizationWithRoles.ID), + wantErr: new(database.MissingConditionError), + }, + { + name: "delete non-existent authorization", + condition: authorizationRepo.PrimaryKeyCondition( + instanceID, + "random-id", + ), + wantRowsAffected: 0, + }, + { + name: "delete authorization with roles", + condition: authorizationRepo.PrimaryKeyCondition( + instanceID, + authorizationWithRoles.ID, + ), + wantRowsAffected: 1, + }, + { + name: "delete authorization without roles", + condition: authorizationRepo.PrimaryKeyCondition( + instanceID, + authorizationWithoutRoles.ID, + ), + wantRowsAffected: 1, + }, + { + name: "delete again", + condition: authorizationRepo.PrimaryKeyCondition( + instanceID, + deletedAuthorization.ID, + ), + wantRowsAffected: 0, + }, + { + name: "delete project grant authorization", + condition: authorizationRepo.PrimaryKeyCondition( + instanceID, + authorizationProjectGrant.ID, + ), + wantRowsAffected: 1, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + savepoint, rollback := savepointForRollback(t, tx) + defer rollback() + + rowsAffected, err := authorizationRepo.Delete(t.Context(), savepoint, tt.condition) + require.ErrorIs(t, err, tt.wantErr) + if err != nil { + return + } + assert.Equal(t, tt.wantRowsAffected, rowsAffected) + + // verify deletion + got, err := authorizationRepo.List(t.Context(), savepoint, database.WithCondition(tt.condition)) + require.NoError(t, err) + require.Empty(t, got) + }) + } +} diff --git a/backend/v3/storage/database/repository/repository_test.go b/backend/v3/storage/database/repository/repository_test.go index 82d89b4e5..22676aec5 100644 --- a/backend/v3/storage/database/repository/repository_test.go +++ b/backend/v3/storage/database/repository/repository_test.go @@ -143,3 +143,21 @@ func createProjectRole(t *testing.T, tx database.Transaction, instanceID, orgID, return projectRole.Key } + +func createProjectGrant(t *testing.T, tx database.Transaction, instanceID, grantingOrgID, grantedOrgID, projectID string, roleKeys []string) string { + t.Helper() + projectGrant := domain.ProjectGrant{ + InstanceID: instanceID, + ID: gofakeit.UUID(), + ProjectID: projectID, + GrantingOrganizationID: grantingOrgID, + GrantedOrganizationID: grantedOrgID, + State: domain.ProjectGrantStateActive, + RoleKeys: roleKeys, + } + projectGrantRepo := repository.ProjectGrantRepository() + err := projectGrantRepo.Create(t.Context(), tx, &projectGrant) + require.NoError(t, err) + + return projectGrant.ID +} diff --git a/internal/query/projection/authorization_relational.go b/internal/query/projection/authorization_relational.go new file mode 100644 index 000000000..b537e53e1 --- /dev/null +++ b/internal/query/projection/authorization_relational.go @@ -0,0 +1,188 @@ +package projection + +import ( + "context" + "database/sql" + + "github.com/muhlemmer/gu" + + repoDomain "github.com/zitadel/zitadel/backend/v3/domain" + v3_sql "github.com/zitadel/zitadel/backend/v3/storage/database/dialect/sql" + "github.com/zitadel/zitadel/backend/v3/storage/database/repository" + "github.com/zitadel/zitadel/internal/eventstore" + "github.com/zitadel/zitadel/internal/eventstore/handler/v2" + "github.com/zitadel/zitadel/internal/repository/usergrant" + "github.com/zitadel/zitadel/internal/zerrors" +) + +const ( + AuthorizationRelationalProjectionTable = "zitadel.authorizations" +) + +type authorizationRelationalProjection struct{} + +func (*authorizationRelationalProjection) Name() string { + return AuthorizationRelationalProjectionTable +} + +func newAuthorizationRelationalProjection(ctx context.Context, config handler.Config) *handler.Handler { + return handler.NewHandler(ctx, &config, new(authorizationRelationalProjection)) +} + +func (a *authorizationRelationalProjection) Reducers() []handler.AggregateReducer { + return []handler.AggregateReducer{ + { + Aggregate: usergrant.AggregateType, + EventReducers: []handler.EventReducer{ + { + Event: usergrant.UserGrantAddedType, + Reduce: a.reduceUserGrantAdded, + }, + { + Event: usergrant.UserGrantChangedType, + Reduce: a.reduceUserGrantChanged, + }, + { + Event: usergrant.UserGrantCascadeChangedType, + Reduce: a.reduceUserGrantChanged, + }, + { + Event: usergrant.UserGrantCascadeRemovedType, + Reduce: a.reduceUserGrantRemoved, + }, + { + Event: usergrant.UserGrantRemovedType, + Reduce: a.reduceUserGrantRemoved, + }, + { + Event: usergrant.UserGrantDeactivatedType, + Reduce: a.reduceUserGrantDeactivated, + }, + { + Event: usergrant.UserGrantReactivatedType, + Reduce: a.reduceUserGrantReactivated, + }, + }, + }, + } +} + +func (a *authorizationRelationalProjection) reduceUserGrantAdded(event eventstore.Event) (*handler.Statement, error) { + e, ok := event.(*usergrant.UserGrantAddedEvent) + if !ok { + return nil, zerrors.ThrowInternalf(nil, "HANDL-an0k9a", "reduce.wrong.event.type %s", usergrant.UserGrantAddedType) + } + return handler.NewStatement(e, func(ctx context.Context, ex handler.Executer, _ string) error { + tx, ok := ex.(*sql.Tx) + if !ok { + return zerrors.ThrowInvalidArgumentf(nil, "HANDL-FrDVPk", "reduce.wrong.db.pool %T", ex) + } + + repo := repository.AuthorizationRepository() + var grantID *string + if e.ProjectGrantID != "" { + grantID = gu.Ptr(e.ProjectGrantID) + } + return repo.Create(ctx, v3_sql.SQLTx(tx), &repoDomain.Authorization{ + InstanceID: e.Aggregate().InstanceID, + ID: e.Aggregate().ID, + UserID: e.UserID, + ProjectID: e.ProjectID, + GrantID: grantID, + Roles: e.RoleKeys, + CreatedAt: e.CreationDate(), + UpdatedAt: e.CreationDate(), + State: repoDomain.AuthorizationStateActive, + }) + }), nil +} + +func (a *authorizationRelationalProjection) reduceUserGrantChanged(event eventstore.Event) (*handler.Statement, error) { + var roles []string + switch e := event.(type) { + case *usergrant.UserGrantChangedEvent: + roles = e.RoleKeys + case *usergrant.UserGrantCascadeChangedEvent: + roles = e.RoleKeys + default: + return nil, zerrors.ThrowInvalidArgumentf(nil, "HANDL-kg1ryL", "reduce.wrong.event.type %v", []eventstore.EventType{usergrant.UserGrantChangedType, usergrant.UserGrantCascadeChangedType}) + } + + return handler.NewStatement(event, func(ctx context.Context, ex handler.Executer, _ string) error { + tx, ok := ex.(*sql.Tx) + if !ok { + return zerrors.ThrowInvalidArgumentf(nil, "HANDL-sBwGEW", "reduce.wrong.db.pool %T", ex) + } + + repo := repository.AuthorizationRepository() + _, err := repo.Update(ctx, v3_sql.SQLTx(tx), + repo.PrimaryKeyCondition(event.Aggregate().InstanceID, event.Aggregate().ID), + roles, + repo.SetUpdatedAt(event.CreatedAt()), + ) + return err + }), nil +} + +func (a *authorizationRelationalProjection) reduceUserGrantRemoved(event eventstore.Event) (*handler.Statement, error) { + switch event.(type) { + case *usergrant.UserGrantRemovedEvent, *usergrant.UserGrantCascadeRemovedEvent: + // ok + default: + return nil, zerrors.ThrowInvalidArgumentf(nil, "HANDL-dFtRl9", "reduce.wrong.event.type %v", []eventstore.EventType{usergrant.UserGrantRemovedType, usergrant.UserGrantCascadeRemovedType}) + } + + return handler.NewStatement(event, func(ctx context.Context, ex handler.Executer, _ string) error { + tx, ok := ex.(*sql.Tx) + if !ok { + return zerrors.ThrowInvalidArgumentf(nil, "HANDL-FlZ55O", "reduce.wrong.db.pool %T", ex) + } + repo := repository.AuthorizationRepository() + _, err := repo.Delete(ctx, v3_sql.SQLTx(tx), + repo.PrimaryKeyCondition(event.Aggregate().InstanceID, event.Aggregate().ID), + ) + return err + }), nil +} + +func (a *authorizationRelationalProjection) reduceUserGrantDeactivated(event eventstore.Event) (*handler.Statement, error) { + _, ok := event.(*usergrant.UserGrantDeactivatedEvent) + if !ok { + return nil, zerrors.ThrowInvalidArgumentf(nil, "HANDL-dFtRl0", "reduce.wrong.event.type %s", usergrant.UserGrantDeactivatedType) + } + return handler.NewStatement(event, func(ctx context.Context, ex handler.Executer, _ string) error { + tx, ok := ex.(*sql.Tx) + if !ok { + return zerrors.ThrowInvalidArgumentf(nil, "HANDL-FlZ55P", "reduce.wrong.db.pool %T", ex) + } + repo := repository.AuthorizationRepository() + _, err := repo.Update(ctx, v3_sql.SQLTx(tx), + repo.PrimaryKeyCondition(event.Aggregate().InstanceID, event.Aggregate().ID), + nil, + repo.SetUpdatedAt(event.CreatedAt()), + repo.SetState(repoDomain.AuthorizationStateInactive), + ) + return err + }), nil +} + +func (a *authorizationRelationalProjection) reduceUserGrantReactivated(event eventstore.Event) (*handler.Statement, error) { + _, ok := event.(*usergrant.UserGrantReactivatedEvent) + if !ok { + return nil, zerrors.ThrowInvalidArgumentf(nil, "HANDL-dFtRl1", "reduce.wrong.event.type %s", usergrant.UserGrantReactivatedType) + } + return handler.NewStatement(event, func(ctx context.Context, ex handler.Executer, _ string) error { + tx, ok := ex.(*sql.Tx) + if !ok { + return zerrors.ThrowInvalidArgumentf(nil, "HANDL-FlZ55Q", "reduce.wrong.db.pool %T", ex) + } + repo := repository.AuthorizationRepository() + _, err := repo.Update(ctx, v3_sql.SQLTx(tx), + repo.PrimaryKeyCondition(event.Aggregate().InstanceID, event.Aggregate().ID), + nil, + repo.SetUpdatedAt(event.CreatedAt()), + repo.SetState(repoDomain.AuthorizationStateActive), + ) + return err + }), nil +} diff --git a/internal/query/projection/projection.go b/internal/query/projection/projection.go index 45b9b436d..0cac0c9ac 100644 --- a/internal/query/projection/projection.go +++ b/internal/query/projection/projection.go @@ -97,6 +97,7 @@ var ( ProjectRelationalProjection *handler.Handler ProjectRoleRelationalProjection *handler.Handler OrganizationMetadataRelationalProjection *handler.Handler + AuthorizationRelationalProjection *handler.Handler ProjectGrantRelationalProjection *handler.Handler SettingsRelationalProjection *handler.Handler @@ -225,6 +226,7 @@ func Create(ctx context.Context, sqlClient *database.DB, es handler.EventStore, ProjectRelationalProjection = newProjectRelationalProjection(ctx, applyCustomConfig(projectionConfig, config.Customizations["projects_relational"])) ProjectRoleRelationalProjection = newProjectRoleRelationalProjection(ctx, applyCustomConfig(projectionConfig, config.Customizations["project_roles_relational"])) OrganizationMetadataRelationalProjection = newOrgMetadataRelationalProjection(ctx, applyCustomConfig(projectionConfig, config.Customizations["organization_metadata_relational"])) + AuthorizationRelationalProjection = newAuthorizationRelationalProjection(ctx, applyCustomConfig(projectionConfig, config.Customizations["authorizations_relational"])) ProjectGrantRelationalProjection = newProjectGrantRelationalProjection(ctx, applyCustomConfig(projectionConfig, config.Customizations["project_grant_relational"])) newProjectionsList() @@ -420,6 +422,7 @@ func newProjectionsList() { ProjectRelationalProjection, ProjectRoleRelationalProjection, OrganizationMetadataRelationalProjection, + AuthorizationRelationalProjection, ProjectGrantRelationalProjection, } }