From f138082b64513955466e8dddc8de2a5a1e4e6c8e Mon Sep 17 00:00:00 2001 From: Silvan <27845747+adlerhurst@users.noreply.github.com> Date: Wed, 18 Mar 2026 10:24:09 +0100 Subject: [PATCH] feat: add administrator repository (#11803) Introduces the `administrators` relational table and repository layer for the v3 backend, unifying instance, organization, project, and project-grant membership into a single scoped model. Closes #10220 ## Changes - **Domain**: `Administrator` struct, `AdministratorScope` enum, `AdministratorRepository` interface with typed CRUD, column, condition, and change helpers. Generated enumer and GoMock. - **Migration** (`016`): `zitadel.administrators` table with a generated `id`, scope-alignment check constraint, cascading foreign keys, and a child `administrator_roles` table. - **Repository**: CTE-based role mutations (`AddRole`, `RemoveRole`, `SetRoles`) for atomicity. Roles aggregated via `ARRAY_AGG`. - **Projection**: Event reducers for `member.added/changed/removed/cascade_removed` across all four admin scopes. - **Fixes**: `Rows.Close()` error wrapping; `TextArray` nil-safe scan + full `pgtype` array interface; `authorization` and `projectGrant` scan helpers fixed for `ARRAY_AGG` under `database/sql`; removed unused `ctx` parameter from `callReduce`. --------- Co-authored-by: Marco Ardizzone --- .golangci.yaml | 17 +- apps/api/project.json | 2 +- backend/v3/domain/administrator.go | 93 ++ .../v3/domain/administratorscope_enumer.go | 117 ++ backend/v3/domain/mock/administrator.mock.go | 1279 +++++++++++++++++ backend/v3/domain/mock/org.mock.go | 39 + .../migration/016_administrators_table.go | 16 + .../016_administrators_table/down.sql | 3 + .../migration/016_administrators_table/up.sql | 78 + .../v3/storage/database/dialect/sql/rows.go | 2 +- .../database/repository/administrator.go | 378 +++++ .../database/repository/administrator_test.go | 665 +++++++++ .../database/repository/authorization.go | 49 +- .../database/repository/project_grant.go | 49 +- internal/database/type.go | 45 + .../projection/administrator_relational.go | 226 +++ .../administrator_relational_test.go | 369 +++++ .../event_testing_relational_test.go | 4 +- .../instance_domain_relational_test.go | 10 +- internal/query/projection/relational_table.go | 70 + .../query/projection/user_relational_test.go | 48 +- 21 files changed, 3513 insertions(+), 46 deletions(-) create mode 100644 backend/v3/domain/administrator.go create mode 100644 backend/v3/domain/administratorscope_enumer.go create mode 100644 backend/v3/domain/mock/administrator.mock.go create mode 100644 backend/v3/storage/database/dialect/postgres/migration/016_administrators_table.go create mode 100644 backend/v3/storage/database/dialect/postgres/migration/016_administrators_table/down.sql create mode 100644 backend/v3/storage/database/dialect/postgres/migration/016_administrators_table/up.sql create mode 100644 backend/v3/storage/database/repository/administrator.go create mode 100644 backend/v3/storage/database/repository/administrator_test.go create mode 100644 internal/query/projection/administrator_relational.go create mode 100644 internal/query/projection/administrator_relational_test.go diff --git a/.golangci.yaml b/.golangci.yaml index 158e45e0a..f1db8a7b3 100644 --- a/.golangci.yaml +++ b/.golangci.yaml @@ -1,9 +1,7 @@ version: "2" run: concurrency: 4 - go: "1.24" - skip-dirs: - - apps/docs + go: "1.25" linters: enable: - asciicheck @@ -94,6 +92,15 @@ linters: - common-false-positives - legacy - std-error-handling + rules: + - path: apps/docs/.* + linters: + - typecheck + # contextcheck requires a context to be passed to the function, but in tests we use `testing.T.Context()` which is not recognized as a context by the linter, so we disable it for the function. + - path: internal/query/projection/.*_relational_test\.go + linters: + - contextcheck + source: callReduce\( paths: - .artifacts - .backups @@ -123,10 +130,6 @@ issues: max-issues-per-linter: 0 max-same-issues: 0 new-from-rev: origin/main - exclude-rules: - - path: apps/docs/.* - linters: - - typecheck formatters: enable: - gci diff --git a/apps/api/project.json b/apps/api/project.json index b9940f9b0..3592ed6df 100644 --- a/apps/api/project.json +++ b/apps/api/project.json @@ -124,7 +124,7 @@ "lint-install": { "description": "Installs golangci-lint binary for linting. Using go install is not recommended in the official docs, because this can produce non-deterministic results.", "cache": true, - "command": "curl -sSfL https://raw.githubusercontent.com/golangci/golangci-lint/HEAD/install.sh | sh -s -- -b .artifacts/bin/$(go env GOOS)/$(go env GOARCH) v2.5.0", + "command": "curl -sSfL https://raw.githubusercontent.com/golangci/golangci-lint/HEAD/install.sh | sh -s -- -b .artifacts/bin/$(go env GOOS)/$(go env GOARCH) v2.11.3", "inputs": [ { "runtime": "go env GOOS" diff --git a/backend/v3/domain/administrator.go b/backend/v3/domain/administrator.go new file mode 100644 index 000000000..2455a2585 --- /dev/null +++ b/backend/v3/domain/administrator.go @@ -0,0 +1,93 @@ +package domain + +import ( + "context" + "time" + + "github.com/zitadel/zitadel/backend/v3/storage/database" +) + +//go:generate enumer -type AdministratorScope -transform snake -trimprefix AdministratorScope -sql +type AdministratorScope uint8 + +const ( + AdministratorScopeInstance AdministratorScope = iota + AdministratorScopeOrganization + AdministratorScopeProject + AdministratorScopeProjectGrant +) + +type Administrator struct { + // ID is generated by the storage layer during creation and any value + // provided by the caller is ignored. The domain package must not + // generate this value. The format is storage-specific, for example + // PostgreSQL uses `::`. + ID string `json:"id,omitempty" db:"id"` + InstanceID string `json:"instanceId,omitempty" db:"instance_id"` + UserID string `json:"userId,omitempty" db:"user_id"` + Scope AdministratorScope `json:"scope,omitempty" db:"scope"` + OrganizationID *string `json:"organizationId,omitempty" db:"organization_id"` + ProjectID *string `json:"projectId,omitempty" db:"project_id"` + ProjectGrantID *string `json:"projectGrantId,omitempty" db:"project_grant_id"` + 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"` +} + +type administratorColumns interface { + PrimaryKeyColumns() []database.Column + IDColumn() database.Column + InstanceIDColumn() database.Column + UserIDColumn() database.Column + ScopeColumn() database.Column + OrganizationIDColumn() database.Column + ProjectIDColumn() database.Column + ProjectGrantIDColumn() database.Column + CreatedAtColumn() database.Column + UpdatedAtColumn() database.Column +} + +type administratorConditions interface { + PrimaryKeyCondition(instanceID, administratorID string) database.Condition + IDCondition(administratorID string) database.Condition + InstanceIDCondition(instanceID string) database.Condition + UserIDCondition(userID string) database.Condition + ScopeCondition(scope AdministratorScope) database.Condition + InstanceAdministratorCondition(instanceID, userID string) database.Condition + OrganizationAdministratorCondition(instanceID, organizationID, userID string) database.Condition + ProjectAdministratorCondition(instanceID, projectID, userID string) database.Condition + ProjectGrantAdministratorCondition(instanceID, projectGrantID, userID string) database.Condition + OrganizationIDCondition(organizationID string) database.Condition + ProjectIDCondition(projectID string) database.Condition + ProjectGrantIDCondition(projectGrantID string) database.Condition + RoleCondition(op database.TextOperation, role string) database.Condition +} + +type administratorChanges interface { + SetUpdatedAt(updatedAt time.Time) database.Change + // AddRole adds the specified role to the administrator. + // If the administrator already has the role, no change will be made. + AddRole(role string) database.Change + // RemoveRole removes the specified role from the administrator. + // If the administrator does not have the role, no change will be made. + RemoveRole(role string) database.Change + // SetRoles replaces all existing roles with the provided ones. + // If no roles are provided, all existing roles will be removed. + SetRoles(roles []string) database.Change +} + +//go:generate mockgen -typed -package domainmock -destination ./mock/administrator.mock.go . AdministratorRepository +type AdministratorRepository interface { + Repository + + administratorColumns + administratorConditions + administratorChanges + + Get(ctx context.Context, client database.QueryExecutor, opts ...database.QueryOption) (*Administrator, error) + List(ctx context.Context, client database.QueryExecutor, opts ...database.QueryOption) ([]*Administrator, error) + + Create(ctx context.Context, client database.QueryExecutor, administrator *Administrator) error + Update(ctx context.Context, client database.QueryExecutor, condition database.Condition, changes ...database.Change) (int64, error) + Delete(ctx context.Context, client database.QueryExecutor, condition database.Condition) (int64, error) +} diff --git a/backend/v3/domain/administratorscope_enumer.go b/backend/v3/domain/administratorscope_enumer.go new file mode 100644 index 000000000..770e0e477 --- /dev/null +++ b/backend/v3/domain/administratorscope_enumer.go @@ -0,0 +1,117 @@ +// Code generated by "enumer -type AdministratorScope -transform snake -trimprefix AdministratorScope -sql"; DO NOT EDIT. + +package domain + +import ( + "database/sql/driver" + "fmt" + "strings" +) + +const _AdministratorScopeName = "instanceorganizationprojectproject_grant" + +var _AdministratorScopeIndex = [...]uint8{0, 8, 20, 27, 40} + +const _AdministratorScopeLowerName = "instanceorganizationprojectproject_grant" + +func (i AdministratorScope) String() string { + if i >= AdministratorScope(len(_AdministratorScopeIndex)-1) { + return fmt.Sprintf("AdministratorScope(%d)", i) + } + return _AdministratorScopeName[_AdministratorScopeIndex[i]:_AdministratorScopeIndex[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 _AdministratorScopeNoOp() { + var x [1]struct{} + _ = x[AdministratorScopeInstance-(0)] + _ = x[AdministratorScopeOrganization-(1)] + _ = x[AdministratorScopeProject-(2)] + _ = x[AdministratorScopeProjectGrant-(3)] +} + +var _AdministratorScopeValues = []AdministratorScope{AdministratorScopeInstance, AdministratorScopeOrganization, AdministratorScopeProject, AdministratorScopeProjectGrant} + +var _AdministratorScopeNameToValueMap = map[string]AdministratorScope{ + _AdministratorScopeName[0:8]: AdministratorScopeInstance, + _AdministratorScopeLowerName[0:8]: AdministratorScopeInstance, + _AdministratorScopeName[8:20]: AdministratorScopeOrganization, + _AdministratorScopeLowerName[8:20]: AdministratorScopeOrganization, + _AdministratorScopeName[20:27]: AdministratorScopeProject, + _AdministratorScopeLowerName[20:27]: AdministratorScopeProject, + _AdministratorScopeName[27:40]: AdministratorScopeProjectGrant, + _AdministratorScopeLowerName[27:40]: AdministratorScopeProjectGrant, +} + +var _AdministratorScopeNames = []string{ + _AdministratorScopeName[0:8], + _AdministratorScopeName[8:20], + _AdministratorScopeName[20:27], + _AdministratorScopeName[27:40], +} + +// AdministratorScopeString retrieves an enum value from the enum constants string name. +// Throws an error if the param is not part of the enum. +func AdministratorScopeString(s string) (AdministratorScope, error) { + if val, ok := _AdministratorScopeNameToValueMap[s]; ok { + return val, nil + } + + if val, ok := _AdministratorScopeNameToValueMap[strings.ToLower(s)]; ok { + return val, nil + } + return 0, fmt.Errorf("%s does not belong to AdministratorScope values", s) +} + +// AdministratorScopeValues returns all values of the enum +func AdministratorScopeValues() []AdministratorScope { + return _AdministratorScopeValues +} + +// AdministratorScopeStrings returns a slice of all String values of the enum +func AdministratorScopeStrings() []string { + strs := make([]string, len(_AdministratorScopeNames)) + copy(strs, _AdministratorScopeNames) + return strs +} + +// IsAAdministratorScope returns "true" if the value is listed in the enum definition. "false" otherwise +func (i AdministratorScope) IsAAdministratorScope() bool { + for _, v := range _AdministratorScopeValues { + if i == v { + return true + } + } + return false +} + +func (i AdministratorScope) Value() (driver.Value, error) { + return i.String(), nil +} + +func (i *AdministratorScope) 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 AdministratorScope: %[1]T(%[1]v)", value) + } + + val, err := AdministratorScopeString(str) + if err != nil { + return err + } + + *i = val + return nil +} diff --git a/backend/v3/domain/mock/administrator.mock.go b/backend/v3/domain/mock/administrator.mock.go new file mode 100644 index 000000000..b5d275062 --- /dev/null +++ b/backend/v3/domain/mock/administrator.mock.go @@ -0,0 +1,1279 @@ +// Code generated by MockGen. DO NOT EDIT. +// Source: github.com/zitadel/zitadel/backend/v3/domain (interfaces: AdministratorRepository) +// +// Generated by this command: +// +// mockgen -typed -package domainmock -destination ./mock/administrator.mock.go . AdministratorRepository +// + +// 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" +) + +// MockAdministratorRepository is a mock of AdministratorRepository interface. +type MockAdministratorRepository struct { + ctrl *gomock.Controller + recorder *MockAdministratorRepositoryMockRecorder + isgomock struct{} +} + +// MockAdministratorRepositoryMockRecorder is the mock recorder for MockAdministratorRepository. +type MockAdministratorRepositoryMockRecorder struct { + mock *MockAdministratorRepository +} + +// NewMockAdministratorRepository creates a new mock instance. +func NewMockAdministratorRepository(ctrl *gomock.Controller) *MockAdministratorRepository { + mock := &MockAdministratorRepository{ctrl: ctrl} + mock.recorder = &MockAdministratorRepositoryMockRecorder{mock} + return mock +} + +// EXPECT returns an object that allows the caller to indicate expected use. +func (m *MockAdministratorRepository) EXPECT() *MockAdministratorRepositoryMockRecorder { + return m.recorder +} + +// AddRole mocks base method. +func (m *MockAdministratorRepository) AddRole(role string) database.Change { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "AddRole", role) + ret0, _ := ret[0].(database.Change) + return ret0 +} + +// AddRole indicates an expected call of AddRole. +func (mr *MockAdministratorRepositoryMockRecorder) AddRole(role any) *MockAdministratorRepositoryAddRoleCall { + mr.mock.ctrl.T.Helper() + call := mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "AddRole", reflect.TypeOf((*MockAdministratorRepository)(nil).AddRole), role) + return &MockAdministratorRepositoryAddRoleCall{Call: call} +} + +// MockAdministratorRepositoryAddRoleCall wrap *gomock.Call +type MockAdministratorRepositoryAddRoleCall struct { + *gomock.Call +} + +// Return rewrite *gomock.Call.Return +func (c *MockAdministratorRepositoryAddRoleCall) Return(arg0 database.Change) *MockAdministratorRepositoryAddRoleCall { + c.Call = c.Call.Return(arg0) + return c +} + +// Do rewrite *gomock.Call.Do +func (c *MockAdministratorRepositoryAddRoleCall) Do(f func(string) database.Change) *MockAdministratorRepositoryAddRoleCall { + c.Call = c.Call.Do(f) + return c +} + +// DoAndReturn rewrite *gomock.Call.DoAndReturn +func (c *MockAdministratorRepositoryAddRoleCall) DoAndReturn(f func(string) database.Change) *MockAdministratorRepositoryAddRoleCall { + c.Call = c.Call.DoAndReturn(f) + return c +} + +// Create mocks base method. +func (m *MockAdministratorRepository) Create(ctx context.Context, client database.QueryExecutor, administrator *domain.Administrator) error { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "Create", ctx, client, administrator) + ret0, _ := ret[0].(error) + return ret0 +} + +// Create indicates an expected call of Create. +func (mr *MockAdministratorRepositoryMockRecorder) Create(ctx, client, administrator any) *MockAdministratorRepositoryCreateCall { + mr.mock.ctrl.T.Helper() + call := mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Create", reflect.TypeOf((*MockAdministratorRepository)(nil).Create), ctx, client, administrator) + return &MockAdministratorRepositoryCreateCall{Call: call} +} + +// MockAdministratorRepositoryCreateCall wrap *gomock.Call +type MockAdministratorRepositoryCreateCall struct { + *gomock.Call +} + +// Return rewrite *gomock.Call.Return +func (c *MockAdministratorRepositoryCreateCall) Return(arg0 error) *MockAdministratorRepositoryCreateCall { + c.Call = c.Call.Return(arg0) + return c +} + +// Do rewrite *gomock.Call.Do +func (c *MockAdministratorRepositoryCreateCall) Do(f func(context.Context, database.QueryExecutor, *domain.Administrator) error) *MockAdministratorRepositoryCreateCall { + c.Call = c.Call.Do(f) + return c +} + +// DoAndReturn rewrite *gomock.Call.DoAndReturn +func (c *MockAdministratorRepositoryCreateCall) DoAndReturn(f func(context.Context, database.QueryExecutor, *domain.Administrator) error) *MockAdministratorRepositoryCreateCall { + c.Call = c.Call.DoAndReturn(f) + return c +} + +// CreatedAtColumn mocks base method. +func (m *MockAdministratorRepository) 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 *MockAdministratorRepositoryMockRecorder) CreatedAtColumn() *MockAdministratorRepositoryCreatedAtColumnCall { + mr.mock.ctrl.T.Helper() + call := mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "CreatedAtColumn", reflect.TypeOf((*MockAdministratorRepository)(nil).CreatedAtColumn)) + return &MockAdministratorRepositoryCreatedAtColumnCall{Call: call} +} + +// MockAdministratorRepositoryCreatedAtColumnCall wrap *gomock.Call +type MockAdministratorRepositoryCreatedAtColumnCall struct { + *gomock.Call +} + +// Return rewrite *gomock.Call.Return +func (c *MockAdministratorRepositoryCreatedAtColumnCall) Return(arg0 database.Column) *MockAdministratorRepositoryCreatedAtColumnCall { + c.Call = c.Call.Return(arg0) + return c +} + +// Do rewrite *gomock.Call.Do +func (c *MockAdministratorRepositoryCreatedAtColumnCall) Do(f func() database.Column) *MockAdministratorRepositoryCreatedAtColumnCall { + c.Call = c.Call.Do(f) + return c +} + +// DoAndReturn rewrite *gomock.Call.DoAndReturn +func (c *MockAdministratorRepositoryCreatedAtColumnCall) DoAndReturn(f func() database.Column) *MockAdministratorRepositoryCreatedAtColumnCall { + c.Call = c.Call.DoAndReturn(f) + return c +} + +// Delete mocks base method. +func (m *MockAdministratorRepository) 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 *MockAdministratorRepositoryMockRecorder) Delete(ctx, client, condition any) *MockAdministratorRepositoryDeleteCall { + mr.mock.ctrl.T.Helper() + call := mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Delete", reflect.TypeOf((*MockAdministratorRepository)(nil).Delete), ctx, client, condition) + return &MockAdministratorRepositoryDeleteCall{Call: call} +} + +// MockAdministratorRepositoryDeleteCall wrap *gomock.Call +type MockAdministratorRepositoryDeleteCall struct { + *gomock.Call +} + +// Return rewrite *gomock.Call.Return +func (c *MockAdministratorRepositoryDeleteCall) Return(arg0 int64, arg1 error) *MockAdministratorRepositoryDeleteCall { + c.Call = c.Call.Return(arg0, arg1) + return c +} + +// Do rewrite *gomock.Call.Do +func (c *MockAdministratorRepositoryDeleteCall) Do(f func(context.Context, database.QueryExecutor, database.Condition) (int64, error)) *MockAdministratorRepositoryDeleteCall { + c.Call = c.Call.Do(f) + return c +} + +// DoAndReturn rewrite *gomock.Call.DoAndReturn +func (c *MockAdministratorRepositoryDeleteCall) DoAndReturn(f func(context.Context, database.QueryExecutor, database.Condition) (int64, error)) *MockAdministratorRepositoryDeleteCall { + c.Call = c.Call.DoAndReturn(f) + return c +} + +// Get mocks base method. +func (m *MockAdministratorRepository) Get(ctx context.Context, client database.QueryExecutor, opts ...database.QueryOption) (*domain.Administrator, 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.Administrator) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// Get indicates an expected call of Get. +func (mr *MockAdministratorRepositoryMockRecorder) Get(ctx, client any, opts ...any) *MockAdministratorRepositoryGetCall { + mr.mock.ctrl.T.Helper() + varargs := append([]any{ctx, client}, opts...) + call := mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Get", reflect.TypeOf((*MockAdministratorRepository)(nil).Get), varargs...) + return &MockAdministratorRepositoryGetCall{Call: call} +} + +// MockAdministratorRepositoryGetCall wrap *gomock.Call +type MockAdministratorRepositoryGetCall struct { + *gomock.Call +} + +// Return rewrite *gomock.Call.Return +func (c *MockAdministratorRepositoryGetCall) Return(arg0 *domain.Administrator, arg1 error) *MockAdministratorRepositoryGetCall { + c.Call = c.Call.Return(arg0, arg1) + return c +} + +// Do rewrite *gomock.Call.Do +func (c *MockAdministratorRepositoryGetCall) Do(f func(context.Context, database.QueryExecutor, ...database.QueryOption) (*domain.Administrator, error)) *MockAdministratorRepositoryGetCall { + c.Call = c.Call.Do(f) + return c +} + +// DoAndReturn rewrite *gomock.Call.DoAndReturn +func (c *MockAdministratorRepositoryGetCall) DoAndReturn(f func(context.Context, database.QueryExecutor, ...database.QueryOption) (*domain.Administrator, error)) *MockAdministratorRepositoryGetCall { + c.Call = c.Call.DoAndReturn(f) + return c +} + +// IDColumn mocks base method. +func (m *MockAdministratorRepository) 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 *MockAdministratorRepositoryMockRecorder) IDColumn() *MockAdministratorRepositoryIDColumnCall { + mr.mock.ctrl.T.Helper() + call := mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "IDColumn", reflect.TypeOf((*MockAdministratorRepository)(nil).IDColumn)) + return &MockAdministratorRepositoryIDColumnCall{Call: call} +} + +// MockAdministratorRepositoryIDColumnCall wrap *gomock.Call +type MockAdministratorRepositoryIDColumnCall struct { + *gomock.Call +} + +// Return rewrite *gomock.Call.Return +func (c *MockAdministratorRepositoryIDColumnCall) Return(arg0 database.Column) *MockAdministratorRepositoryIDColumnCall { + c.Call = c.Call.Return(arg0) + return c +} + +// Do rewrite *gomock.Call.Do +func (c *MockAdministratorRepositoryIDColumnCall) Do(f func() database.Column) *MockAdministratorRepositoryIDColumnCall { + c.Call = c.Call.Do(f) + return c +} + +// DoAndReturn rewrite *gomock.Call.DoAndReturn +func (c *MockAdministratorRepositoryIDColumnCall) DoAndReturn(f func() database.Column) *MockAdministratorRepositoryIDColumnCall { + c.Call = c.Call.DoAndReturn(f) + return c +} + +// IDCondition mocks base method. +func (m *MockAdministratorRepository) IDCondition(administratorID string) database.Condition { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "IDCondition", administratorID) + ret0, _ := ret[0].(database.Condition) + return ret0 +} + +// IDCondition indicates an expected call of IDCondition. +func (mr *MockAdministratorRepositoryMockRecorder) IDCondition(administratorID any) *MockAdministratorRepositoryIDConditionCall { + mr.mock.ctrl.T.Helper() + call := mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "IDCondition", reflect.TypeOf((*MockAdministratorRepository)(nil).IDCondition), administratorID) + return &MockAdministratorRepositoryIDConditionCall{Call: call} +} + +// MockAdministratorRepositoryIDConditionCall wrap *gomock.Call +type MockAdministratorRepositoryIDConditionCall struct { + *gomock.Call +} + +// Return rewrite *gomock.Call.Return +func (c *MockAdministratorRepositoryIDConditionCall) Return(arg0 database.Condition) *MockAdministratorRepositoryIDConditionCall { + c.Call = c.Call.Return(arg0) + return c +} + +// Do rewrite *gomock.Call.Do +func (c *MockAdministratorRepositoryIDConditionCall) Do(f func(string) database.Condition) *MockAdministratorRepositoryIDConditionCall { + c.Call = c.Call.Do(f) + return c +} + +// DoAndReturn rewrite *gomock.Call.DoAndReturn +func (c *MockAdministratorRepositoryIDConditionCall) DoAndReturn(f func(string) database.Condition) *MockAdministratorRepositoryIDConditionCall { + c.Call = c.Call.DoAndReturn(f) + return c +} + +// InstanceAdministratorCondition mocks base method. +func (m *MockAdministratorRepository) InstanceAdministratorCondition(instanceID, userID string) database.Condition { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "InstanceAdministratorCondition", instanceID, userID) + ret0, _ := ret[0].(database.Condition) + return ret0 +} + +// InstanceAdministratorCondition indicates an expected call of InstanceAdministratorCondition. +func (mr *MockAdministratorRepositoryMockRecorder) InstanceAdministratorCondition(instanceID, userID any) *MockAdministratorRepositoryInstanceAdministratorConditionCall { + mr.mock.ctrl.T.Helper() + call := mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "InstanceAdministratorCondition", reflect.TypeOf((*MockAdministratorRepository)(nil).InstanceAdministratorCondition), instanceID, userID) + return &MockAdministratorRepositoryInstanceAdministratorConditionCall{Call: call} +} + +// MockAdministratorRepositoryInstanceAdministratorConditionCall wrap *gomock.Call +type MockAdministratorRepositoryInstanceAdministratorConditionCall struct { + *gomock.Call +} + +// Return rewrite *gomock.Call.Return +func (c *MockAdministratorRepositoryInstanceAdministratorConditionCall) Return(arg0 database.Condition) *MockAdministratorRepositoryInstanceAdministratorConditionCall { + c.Call = c.Call.Return(arg0) + return c +} + +// Do rewrite *gomock.Call.Do +func (c *MockAdministratorRepositoryInstanceAdministratorConditionCall) Do(f func(string, string) database.Condition) *MockAdministratorRepositoryInstanceAdministratorConditionCall { + c.Call = c.Call.Do(f) + return c +} + +// DoAndReturn rewrite *gomock.Call.DoAndReturn +func (c *MockAdministratorRepositoryInstanceAdministratorConditionCall) DoAndReturn(f func(string, string) database.Condition) *MockAdministratorRepositoryInstanceAdministratorConditionCall { + c.Call = c.Call.DoAndReturn(f) + return c +} + +// InstanceIDColumn mocks base method. +func (m *MockAdministratorRepository) 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 *MockAdministratorRepositoryMockRecorder) InstanceIDColumn() *MockAdministratorRepositoryInstanceIDColumnCall { + mr.mock.ctrl.T.Helper() + call := mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "InstanceIDColumn", reflect.TypeOf((*MockAdministratorRepository)(nil).InstanceIDColumn)) + return &MockAdministratorRepositoryInstanceIDColumnCall{Call: call} +} + +// MockAdministratorRepositoryInstanceIDColumnCall wrap *gomock.Call +type MockAdministratorRepositoryInstanceIDColumnCall struct { + *gomock.Call +} + +// Return rewrite *gomock.Call.Return +func (c *MockAdministratorRepositoryInstanceIDColumnCall) Return(arg0 database.Column) *MockAdministratorRepositoryInstanceIDColumnCall { + c.Call = c.Call.Return(arg0) + return c +} + +// Do rewrite *gomock.Call.Do +func (c *MockAdministratorRepositoryInstanceIDColumnCall) Do(f func() database.Column) *MockAdministratorRepositoryInstanceIDColumnCall { + c.Call = c.Call.Do(f) + return c +} + +// DoAndReturn rewrite *gomock.Call.DoAndReturn +func (c *MockAdministratorRepositoryInstanceIDColumnCall) DoAndReturn(f func() database.Column) *MockAdministratorRepositoryInstanceIDColumnCall { + c.Call = c.Call.DoAndReturn(f) + return c +} + +// InstanceIDCondition mocks base method. +func (m *MockAdministratorRepository) 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 *MockAdministratorRepositoryMockRecorder) InstanceIDCondition(instanceID any) *MockAdministratorRepositoryInstanceIDConditionCall { + mr.mock.ctrl.T.Helper() + call := mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "InstanceIDCondition", reflect.TypeOf((*MockAdministratorRepository)(nil).InstanceIDCondition), instanceID) + return &MockAdministratorRepositoryInstanceIDConditionCall{Call: call} +} + +// MockAdministratorRepositoryInstanceIDConditionCall wrap *gomock.Call +type MockAdministratorRepositoryInstanceIDConditionCall struct { + *gomock.Call +} + +// Return rewrite *gomock.Call.Return +func (c *MockAdministratorRepositoryInstanceIDConditionCall) Return(arg0 database.Condition) *MockAdministratorRepositoryInstanceIDConditionCall { + c.Call = c.Call.Return(arg0) + return c +} + +// Do rewrite *gomock.Call.Do +func (c *MockAdministratorRepositoryInstanceIDConditionCall) Do(f func(string) database.Condition) *MockAdministratorRepositoryInstanceIDConditionCall { + c.Call = c.Call.Do(f) + return c +} + +// DoAndReturn rewrite *gomock.Call.DoAndReturn +func (c *MockAdministratorRepositoryInstanceIDConditionCall) DoAndReturn(f func(string) database.Condition) *MockAdministratorRepositoryInstanceIDConditionCall { + c.Call = c.Call.DoAndReturn(f) + return c +} + +// List mocks base method. +func (m *MockAdministratorRepository) List(ctx context.Context, client database.QueryExecutor, opts ...database.QueryOption) ([]*domain.Administrator, 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.Administrator) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// List indicates an expected call of List. +func (mr *MockAdministratorRepositoryMockRecorder) List(ctx, client any, opts ...any) *MockAdministratorRepositoryListCall { + mr.mock.ctrl.T.Helper() + varargs := append([]any{ctx, client}, opts...) + call := mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "List", reflect.TypeOf((*MockAdministratorRepository)(nil).List), varargs...) + return &MockAdministratorRepositoryListCall{Call: call} +} + +// MockAdministratorRepositoryListCall wrap *gomock.Call +type MockAdministratorRepositoryListCall struct { + *gomock.Call +} + +// Return rewrite *gomock.Call.Return +func (c *MockAdministratorRepositoryListCall) Return(arg0 []*domain.Administrator, arg1 error) *MockAdministratorRepositoryListCall { + c.Call = c.Call.Return(arg0, arg1) + return c +} + +// Do rewrite *gomock.Call.Do +func (c *MockAdministratorRepositoryListCall) Do(f func(context.Context, database.QueryExecutor, ...database.QueryOption) ([]*domain.Administrator, error)) *MockAdministratorRepositoryListCall { + c.Call = c.Call.Do(f) + return c +} + +// DoAndReturn rewrite *gomock.Call.DoAndReturn +func (c *MockAdministratorRepositoryListCall) DoAndReturn(f func(context.Context, database.QueryExecutor, ...database.QueryOption) ([]*domain.Administrator, error)) *MockAdministratorRepositoryListCall { + c.Call = c.Call.DoAndReturn(f) + return c +} + +// OrganizationAdministratorCondition mocks base method. +func (m *MockAdministratorRepository) OrganizationAdministratorCondition(instanceID, organizationID, userID string) database.Condition { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "OrganizationAdministratorCondition", instanceID, organizationID, userID) + ret0, _ := ret[0].(database.Condition) + return ret0 +} + +// OrganizationAdministratorCondition indicates an expected call of OrganizationAdministratorCondition. +func (mr *MockAdministratorRepositoryMockRecorder) OrganizationAdministratorCondition(instanceID, organizationID, userID any) *MockAdministratorRepositoryOrganizationAdministratorConditionCall { + mr.mock.ctrl.T.Helper() + call := mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "OrganizationAdministratorCondition", reflect.TypeOf((*MockAdministratorRepository)(nil).OrganizationAdministratorCondition), instanceID, organizationID, userID) + return &MockAdministratorRepositoryOrganizationAdministratorConditionCall{Call: call} +} + +// MockAdministratorRepositoryOrganizationAdministratorConditionCall wrap *gomock.Call +type MockAdministratorRepositoryOrganizationAdministratorConditionCall struct { + *gomock.Call +} + +// Return rewrite *gomock.Call.Return +func (c *MockAdministratorRepositoryOrganizationAdministratorConditionCall) Return(arg0 database.Condition) *MockAdministratorRepositoryOrganizationAdministratorConditionCall { + c.Call = c.Call.Return(arg0) + return c +} + +// Do rewrite *gomock.Call.Do +func (c *MockAdministratorRepositoryOrganizationAdministratorConditionCall) Do(f func(string, string, string) database.Condition) *MockAdministratorRepositoryOrganizationAdministratorConditionCall { + c.Call = c.Call.Do(f) + return c +} + +// DoAndReturn rewrite *gomock.Call.DoAndReturn +func (c *MockAdministratorRepositoryOrganizationAdministratorConditionCall) DoAndReturn(f func(string, string, string) database.Condition) *MockAdministratorRepositoryOrganizationAdministratorConditionCall { + c.Call = c.Call.DoAndReturn(f) + return c +} + +// OrganizationIDColumn mocks base method. +func (m *MockAdministratorRepository) OrganizationIDColumn() database.Column { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "OrganizationIDColumn") + ret0, _ := ret[0].(database.Column) + return ret0 +} + +// OrganizationIDColumn indicates an expected call of OrganizationIDColumn. +func (mr *MockAdministratorRepositoryMockRecorder) OrganizationIDColumn() *MockAdministratorRepositoryOrganizationIDColumnCall { + mr.mock.ctrl.T.Helper() + call := mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "OrganizationIDColumn", reflect.TypeOf((*MockAdministratorRepository)(nil).OrganizationIDColumn)) + return &MockAdministratorRepositoryOrganizationIDColumnCall{Call: call} +} + +// MockAdministratorRepositoryOrganizationIDColumnCall wrap *gomock.Call +type MockAdministratorRepositoryOrganizationIDColumnCall struct { + *gomock.Call +} + +// Return rewrite *gomock.Call.Return +func (c *MockAdministratorRepositoryOrganizationIDColumnCall) Return(arg0 database.Column) *MockAdministratorRepositoryOrganizationIDColumnCall { + c.Call = c.Call.Return(arg0) + return c +} + +// Do rewrite *gomock.Call.Do +func (c *MockAdministratorRepositoryOrganizationIDColumnCall) Do(f func() database.Column) *MockAdministratorRepositoryOrganizationIDColumnCall { + c.Call = c.Call.Do(f) + return c +} + +// DoAndReturn rewrite *gomock.Call.DoAndReturn +func (c *MockAdministratorRepositoryOrganizationIDColumnCall) DoAndReturn(f func() database.Column) *MockAdministratorRepositoryOrganizationIDColumnCall { + c.Call = c.Call.DoAndReturn(f) + return c +} + +// OrganizationIDCondition mocks base method. +func (m *MockAdministratorRepository) OrganizationIDCondition(organizationID string) database.Condition { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "OrganizationIDCondition", organizationID) + ret0, _ := ret[0].(database.Condition) + return ret0 +} + +// OrganizationIDCondition indicates an expected call of OrganizationIDCondition. +func (mr *MockAdministratorRepositoryMockRecorder) OrganizationIDCondition(organizationID any) *MockAdministratorRepositoryOrganizationIDConditionCall { + mr.mock.ctrl.T.Helper() + call := mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "OrganizationIDCondition", reflect.TypeOf((*MockAdministratorRepository)(nil).OrganizationIDCondition), organizationID) + return &MockAdministratorRepositoryOrganizationIDConditionCall{Call: call} +} + +// MockAdministratorRepositoryOrganizationIDConditionCall wrap *gomock.Call +type MockAdministratorRepositoryOrganizationIDConditionCall struct { + *gomock.Call +} + +// Return rewrite *gomock.Call.Return +func (c *MockAdministratorRepositoryOrganizationIDConditionCall) Return(arg0 database.Condition) *MockAdministratorRepositoryOrganizationIDConditionCall { + c.Call = c.Call.Return(arg0) + return c +} + +// Do rewrite *gomock.Call.Do +func (c *MockAdministratorRepositoryOrganizationIDConditionCall) Do(f func(string) database.Condition) *MockAdministratorRepositoryOrganizationIDConditionCall { + c.Call = c.Call.Do(f) + return c +} + +// DoAndReturn rewrite *gomock.Call.DoAndReturn +func (c *MockAdministratorRepositoryOrganizationIDConditionCall) DoAndReturn(f func(string) database.Condition) *MockAdministratorRepositoryOrganizationIDConditionCall { + c.Call = c.Call.DoAndReturn(f) + return c +} + +// PrimaryKeyColumns mocks base method. +func (m *MockAdministratorRepository) 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 *MockAdministratorRepositoryMockRecorder) PrimaryKeyColumns() *MockAdministratorRepositoryPrimaryKeyColumnsCall { + mr.mock.ctrl.T.Helper() + call := mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "PrimaryKeyColumns", reflect.TypeOf((*MockAdministratorRepository)(nil).PrimaryKeyColumns)) + return &MockAdministratorRepositoryPrimaryKeyColumnsCall{Call: call} +} + +// MockAdministratorRepositoryPrimaryKeyColumnsCall wrap *gomock.Call +type MockAdministratorRepositoryPrimaryKeyColumnsCall struct { + *gomock.Call +} + +// Return rewrite *gomock.Call.Return +func (c *MockAdministratorRepositoryPrimaryKeyColumnsCall) Return(arg0 []database.Column) *MockAdministratorRepositoryPrimaryKeyColumnsCall { + c.Call = c.Call.Return(arg0) + return c +} + +// Do rewrite *gomock.Call.Do +func (c *MockAdministratorRepositoryPrimaryKeyColumnsCall) Do(f func() []database.Column) *MockAdministratorRepositoryPrimaryKeyColumnsCall { + c.Call = c.Call.Do(f) + return c +} + +// DoAndReturn rewrite *gomock.Call.DoAndReturn +func (c *MockAdministratorRepositoryPrimaryKeyColumnsCall) DoAndReturn(f func() []database.Column) *MockAdministratorRepositoryPrimaryKeyColumnsCall { + c.Call = c.Call.DoAndReturn(f) + return c +} + +// PrimaryKeyCondition mocks base method. +func (m *MockAdministratorRepository) PrimaryKeyCondition(instanceID, administratorID string) database.Condition { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "PrimaryKeyCondition", instanceID, administratorID) + ret0, _ := ret[0].(database.Condition) + return ret0 +} + +// PrimaryKeyCondition indicates an expected call of PrimaryKeyCondition. +func (mr *MockAdministratorRepositoryMockRecorder) PrimaryKeyCondition(instanceID, administratorID any) *MockAdministratorRepositoryPrimaryKeyConditionCall { + mr.mock.ctrl.T.Helper() + call := mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "PrimaryKeyCondition", reflect.TypeOf((*MockAdministratorRepository)(nil).PrimaryKeyCondition), instanceID, administratorID) + return &MockAdministratorRepositoryPrimaryKeyConditionCall{Call: call} +} + +// MockAdministratorRepositoryPrimaryKeyConditionCall wrap *gomock.Call +type MockAdministratorRepositoryPrimaryKeyConditionCall struct { + *gomock.Call +} + +// Return rewrite *gomock.Call.Return +func (c *MockAdministratorRepositoryPrimaryKeyConditionCall) Return(arg0 database.Condition) *MockAdministratorRepositoryPrimaryKeyConditionCall { + c.Call = c.Call.Return(arg0) + return c +} + +// Do rewrite *gomock.Call.Do +func (c *MockAdministratorRepositoryPrimaryKeyConditionCall) Do(f func(string, string) database.Condition) *MockAdministratorRepositoryPrimaryKeyConditionCall { + c.Call = c.Call.Do(f) + return c +} + +// DoAndReturn rewrite *gomock.Call.DoAndReturn +func (c *MockAdministratorRepositoryPrimaryKeyConditionCall) DoAndReturn(f func(string, string) database.Condition) *MockAdministratorRepositoryPrimaryKeyConditionCall { + c.Call = c.Call.DoAndReturn(f) + return c +} + +// ProjectAdministratorCondition mocks base method. +func (m *MockAdministratorRepository) ProjectAdministratorCondition(instanceID, projectID, userID string) database.Condition { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "ProjectAdministratorCondition", instanceID, projectID, userID) + ret0, _ := ret[0].(database.Condition) + return ret0 +} + +// ProjectAdministratorCondition indicates an expected call of ProjectAdministratorCondition. +func (mr *MockAdministratorRepositoryMockRecorder) ProjectAdministratorCondition(instanceID, projectID, userID any) *MockAdministratorRepositoryProjectAdministratorConditionCall { + mr.mock.ctrl.T.Helper() + call := mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ProjectAdministratorCondition", reflect.TypeOf((*MockAdministratorRepository)(nil).ProjectAdministratorCondition), instanceID, projectID, userID) + return &MockAdministratorRepositoryProjectAdministratorConditionCall{Call: call} +} + +// MockAdministratorRepositoryProjectAdministratorConditionCall wrap *gomock.Call +type MockAdministratorRepositoryProjectAdministratorConditionCall struct { + *gomock.Call +} + +// Return rewrite *gomock.Call.Return +func (c *MockAdministratorRepositoryProjectAdministratorConditionCall) Return(arg0 database.Condition) *MockAdministratorRepositoryProjectAdministratorConditionCall { + c.Call = c.Call.Return(arg0) + return c +} + +// Do rewrite *gomock.Call.Do +func (c *MockAdministratorRepositoryProjectAdministratorConditionCall) Do(f func(string, string, string) database.Condition) *MockAdministratorRepositoryProjectAdministratorConditionCall { + c.Call = c.Call.Do(f) + return c +} + +// DoAndReturn rewrite *gomock.Call.DoAndReturn +func (c *MockAdministratorRepositoryProjectAdministratorConditionCall) DoAndReturn(f func(string, string, string) database.Condition) *MockAdministratorRepositoryProjectAdministratorConditionCall { + c.Call = c.Call.DoAndReturn(f) + return c +} + +// ProjectGrantAdministratorCondition mocks base method. +func (m *MockAdministratorRepository) ProjectGrantAdministratorCondition(instanceID, projectGrantID, userID string) database.Condition { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "ProjectGrantAdministratorCondition", instanceID, projectGrantID, userID) + ret0, _ := ret[0].(database.Condition) + return ret0 +} + +// ProjectGrantAdministratorCondition indicates an expected call of ProjectGrantAdministratorCondition. +func (mr *MockAdministratorRepositoryMockRecorder) ProjectGrantAdministratorCondition(instanceID, projectGrantID, userID any) *MockAdministratorRepositoryProjectGrantAdministratorConditionCall { + mr.mock.ctrl.T.Helper() + call := mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ProjectGrantAdministratorCondition", reflect.TypeOf((*MockAdministratorRepository)(nil).ProjectGrantAdministratorCondition), instanceID, projectGrantID, userID) + return &MockAdministratorRepositoryProjectGrantAdministratorConditionCall{Call: call} +} + +// MockAdministratorRepositoryProjectGrantAdministratorConditionCall wrap *gomock.Call +type MockAdministratorRepositoryProjectGrantAdministratorConditionCall struct { + *gomock.Call +} + +// Return rewrite *gomock.Call.Return +func (c *MockAdministratorRepositoryProjectGrantAdministratorConditionCall) Return(arg0 database.Condition) *MockAdministratorRepositoryProjectGrantAdministratorConditionCall { + c.Call = c.Call.Return(arg0) + return c +} + +// Do rewrite *gomock.Call.Do +func (c *MockAdministratorRepositoryProjectGrantAdministratorConditionCall) Do(f func(string, string, string) database.Condition) *MockAdministratorRepositoryProjectGrantAdministratorConditionCall { + c.Call = c.Call.Do(f) + return c +} + +// DoAndReturn rewrite *gomock.Call.DoAndReturn +func (c *MockAdministratorRepositoryProjectGrantAdministratorConditionCall) DoAndReturn(f func(string, string, string) database.Condition) *MockAdministratorRepositoryProjectGrantAdministratorConditionCall { + c.Call = c.Call.DoAndReturn(f) + return c +} + +// ProjectGrantIDColumn mocks base method. +func (m *MockAdministratorRepository) ProjectGrantIDColumn() database.Column { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "ProjectGrantIDColumn") + ret0, _ := ret[0].(database.Column) + return ret0 +} + +// ProjectGrantIDColumn indicates an expected call of ProjectGrantIDColumn. +func (mr *MockAdministratorRepositoryMockRecorder) ProjectGrantIDColumn() *MockAdministratorRepositoryProjectGrantIDColumnCall { + mr.mock.ctrl.T.Helper() + call := mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ProjectGrantIDColumn", reflect.TypeOf((*MockAdministratorRepository)(nil).ProjectGrantIDColumn)) + return &MockAdministratorRepositoryProjectGrantIDColumnCall{Call: call} +} + +// MockAdministratorRepositoryProjectGrantIDColumnCall wrap *gomock.Call +type MockAdministratorRepositoryProjectGrantIDColumnCall struct { + *gomock.Call +} + +// Return rewrite *gomock.Call.Return +func (c *MockAdministratorRepositoryProjectGrantIDColumnCall) Return(arg0 database.Column) *MockAdministratorRepositoryProjectGrantIDColumnCall { + c.Call = c.Call.Return(arg0) + return c +} + +// Do rewrite *gomock.Call.Do +func (c *MockAdministratorRepositoryProjectGrantIDColumnCall) Do(f func() database.Column) *MockAdministratorRepositoryProjectGrantIDColumnCall { + c.Call = c.Call.Do(f) + return c +} + +// DoAndReturn rewrite *gomock.Call.DoAndReturn +func (c *MockAdministratorRepositoryProjectGrantIDColumnCall) DoAndReturn(f func() database.Column) *MockAdministratorRepositoryProjectGrantIDColumnCall { + c.Call = c.Call.DoAndReturn(f) + return c +} + +// ProjectGrantIDCondition mocks base method. +func (m *MockAdministratorRepository) ProjectGrantIDCondition(projectGrantID string) database.Condition { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "ProjectGrantIDCondition", projectGrantID) + ret0, _ := ret[0].(database.Condition) + return ret0 +} + +// ProjectGrantIDCondition indicates an expected call of ProjectGrantIDCondition. +func (mr *MockAdministratorRepositoryMockRecorder) ProjectGrantIDCondition(projectGrantID any) *MockAdministratorRepositoryProjectGrantIDConditionCall { + mr.mock.ctrl.T.Helper() + call := mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ProjectGrantIDCondition", reflect.TypeOf((*MockAdministratorRepository)(nil).ProjectGrantIDCondition), projectGrantID) + return &MockAdministratorRepositoryProjectGrantIDConditionCall{Call: call} +} + +// MockAdministratorRepositoryProjectGrantIDConditionCall wrap *gomock.Call +type MockAdministratorRepositoryProjectGrantIDConditionCall struct { + *gomock.Call +} + +// Return rewrite *gomock.Call.Return +func (c *MockAdministratorRepositoryProjectGrantIDConditionCall) Return(arg0 database.Condition) *MockAdministratorRepositoryProjectGrantIDConditionCall { + c.Call = c.Call.Return(arg0) + return c +} + +// Do rewrite *gomock.Call.Do +func (c *MockAdministratorRepositoryProjectGrantIDConditionCall) Do(f func(string) database.Condition) *MockAdministratorRepositoryProjectGrantIDConditionCall { + c.Call = c.Call.Do(f) + return c +} + +// DoAndReturn rewrite *gomock.Call.DoAndReturn +func (c *MockAdministratorRepositoryProjectGrantIDConditionCall) DoAndReturn(f func(string) database.Condition) *MockAdministratorRepositoryProjectGrantIDConditionCall { + c.Call = c.Call.DoAndReturn(f) + return c +} + +// ProjectIDColumn mocks base method. +func (m *MockAdministratorRepository) 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 *MockAdministratorRepositoryMockRecorder) ProjectIDColumn() *MockAdministratorRepositoryProjectIDColumnCall { + mr.mock.ctrl.T.Helper() + call := mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ProjectIDColumn", reflect.TypeOf((*MockAdministratorRepository)(nil).ProjectIDColumn)) + return &MockAdministratorRepositoryProjectIDColumnCall{Call: call} +} + +// MockAdministratorRepositoryProjectIDColumnCall wrap *gomock.Call +type MockAdministratorRepositoryProjectIDColumnCall struct { + *gomock.Call +} + +// Return rewrite *gomock.Call.Return +func (c *MockAdministratorRepositoryProjectIDColumnCall) Return(arg0 database.Column) *MockAdministratorRepositoryProjectIDColumnCall { + c.Call = c.Call.Return(arg0) + return c +} + +// Do rewrite *gomock.Call.Do +func (c *MockAdministratorRepositoryProjectIDColumnCall) Do(f func() database.Column) *MockAdministratorRepositoryProjectIDColumnCall { + c.Call = c.Call.Do(f) + return c +} + +// DoAndReturn rewrite *gomock.Call.DoAndReturn +func (c *MockAdministratorRepositoryProjectIDColumnCall) DoAndReturn(f func() database.Column) *MockAdministratorRepositoryProjectIDColumnCall { + c.Call = c.Call.DoAndReturn(f) + return c +} + +// ProjectIDCondition mocks base method. +func (m *MockAdministratorRepository) 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 *MockAdministratorRepositoryMockRecorder) ProjectIDCondition(projectID any) *MockAdministratorRepositoryProjectIDConditionCall { + mr.mock.ctrl.T.Helper() + call := mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ProjectIDCondition", reflect.TypeOf((*MockAdministratorRepository)(nil).ProjectIDCondition), projectID) + return &MockAdministratorRepositoryProjectIDConditionCall{Call: call} +} + +// MockAdministratorRepositoryProjectIDConditionCall wrap *gomock.Call +type MockAdministratorRepositoryProjectIDConditionCall struct { + *gomock.Call +} + +// Return rewrite *gomock.Call.Return +func (c *MockAdministratorRepositoryProjectIDConditionCall) Return(arg0 database.Condition) *MockAdministratorRepositoryProjectIDConditionCall { + c.Call = c.Call.Return(arg0) + return c +} + +// Do rewrite *gomock.Call.Do +func (c *MockAdministratorRepositoryProjectIDConditionCall) Do(f func(string) database.Condition) *MockAdministratorRepositoryProjectIDConditionCall { + c.Call = c.Call.Do(f) + return c +} + +// DoAndReturn rewrite *gomock.Call.DoAndReturn +func (c *MockAdministratorRepositoryProjectIDConditionCall) DoAndReturn(f func(string) database.Condition) *MockAdministratorRepositoryProjectIDConditionCall { + c.Call = c.Call.DoAndReturn(f) + return c +} + +// RemoveRole mocks base method. +func (m *MockAdministratorRepository) RemoveRole(role string) database.Change { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "RemoveRole", role) + ret0, _ := ret[0].(database.Change) + return ret0 +} + +// RemoveRole indicates an expected call of RemoveRole. +func (mr *MockAdministratorRepositoryMockRecorder) RemoveRole(role any) *MockAdministratorRepositoryRemoveRoleCall { + mr.mock.ctrl.T.Helper() + call := mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "RemoveRole", reflect.TypeOf((*MockAdministratorRepository)(nil).RemoveRole), role) + return &MockAdministratorRepositoryRemoveRoleCall{Call: call} +} + +// MockAdministratorRepositoryRemoveRoleCall wrap *gomock.Call +type MockAdministratorRepositoryRemoveRoleCall struct { + *gomock.Call +} + +// Return rewrite *gomock.Call.Return +func (c *MockAdministratorRepositoryRemoveRoleCall) Return(arg0 database.Change) *MockAdministratorRepositoryRemoveRoleCall { + c.Call = c.Call.Return(arg0) + return c +} + +// Do rewrite *gomock.Call.Do +func (c *MockAdministratorRepositoryRemoveRoleCall) Do(f func(string) database.Change) *MockAdministratorRepositoryRemoveRoleCall { + c.Call = c.Call.Do(f) + return c +} + +// DoAndReturn rewrite *gomock.Call.DoAndReturn +func (c *MockAdministratorRepositoryRemoveRoleCall) DoAndReturn(f func(string) database.Change) *MockAdministratorRepositoryRemoveRoleCall { + c.Call = c.Call.DoAndReturn(f) + return c +} + +// RoleCondition mocks base method. +func (m *MockAdministratorRepository) 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 *MockAdministratorRepositoryMockRecorder) RoleCondition(op, role any) *MockAdministratorRepositoryRoleConditionCall { + mr.mock.ctrl.T.Helper() + call := mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "RoleCondition", reflect.TypeOf((*MockAdministratorRepository)(nil).RoleCondition), op, role) + return &MockAdministratorRepositoryRoleConditionCall{Call: call} +} + +// MockAdministratorRepositoryRoleConditionCall wrap *gomock.Call +type MockAdministratorRepositoryRoleConditionCall struct { + *gomock.Call +} + +// Return rewrite *gomock.Call.Return +func (c *MockAdministratorRepositoryRoleConditionCall) Return(arg0 database.Condition) *MockAdministratorRepositoryRoleConditionCall { + c.Call = c.Call.Return(arg0) + return c +} + +// Do rewrite *gomock.Call.Do +func (c *MockAdministratorRepositoryRoleConditionCall) Do(f func(database.TextOperation, string) database.Condition) *MockAdministratorRepositoryRoleConditionCall { + c.Call = c.Call.Do(f) + return c +} + +// DoAndReturn rewrite *gomock.Call.DoAndReturn +func (c *MockAdministratorRepositoryRoleConditionCall) DoAndReturn(f func(database.TextOperation, string) database.Condition) *MockAdministratorRepositoryRoleConditionCall { + c.Call = c.Call.DoAndReturn(f) + return c +} + +// ScopeColumn mocks base method. +func (m *MockAdministratorRepository) ScopeColumn() database.Column { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "ScopeColumn") + ret0, _ := ret[0].(database.Column) + return ret0 +} + +// ScopeColumn indicates an expected call of ScopeColumn. +func (mr *MockAdministratorRepositoryMockRecorder) ScopeColumn() *MockAdministratorRepositoryScopeColumnCall { + mr.mock.ctrl.T.Helper() + call := mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ScopeColumn", reflect.TypeOf((*MockAdministratorRepository)(nil).ScopeColumn)) + return &MockAdministratorRepositoryScopeColumnCall{Call: call} +} + +// MockAdministratorRepositoryScopeColumnCall wrap *gomock.Call +type MockAdministratorRepositoryScopeColumnCall struct { + *gomock.Call +} + +// Return rewrite *gomock.Call.Return +func (c *MockAdministratorRepositoryScopeColumnCall) Return(arg0 database.Column) *MockAdministratorRepositoryScopeColumnCall { + c.Call = c.Call.Return(arg0) + return c +} + +// Do rewrite *gomock.Call.Do +func (c *MockAdministratorRepositoryScopeColumnCall) Do(f func() database.Column) *MockAdministratorRepositoryScopeColumnCall { + c.Call = c.Call.Do(f) + return c +} + +// DoAndReturn rewrite *gomock.Call.DoAndReturn +func (c *MockAdministratorRepositoryScopeColumnCall) DoAndReturn(f func() database.Column) *MockAdministratorRepositoryScopeColumnCall { + c.Call = c.Call.DoAndReturn(f) + return c +} + +// ScopeCondition mocks base method. +func (m *MockAdministratorRepository) ScopeCondition(scope domain.AdministratorScope) database.Condition { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "ScopeCondition", scope) + ret0, _ := ret[0].(database.Condition) + return ret0 +} + +// ScopeCondition indicates an expected call of ScopeCondition. +func (mr *MockAdministratorRepositoryMockRecorder) ScopeCondition(scope any) *MockAdministratorRepositoryScopeConditionCall { + mr.mock.ctrl.T.Helper() + call := mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ScopeCondition", reflect.TypeOf((*MockAdministratorRepository)(nil).ScopeCondition), scope) + return &MockAdministratorRepositoryScopeConditionCall{Call: call} +} + +// MockAdministratorRepositoryScopeConditionCall wrap *gomock.Call +type MockAdministratorRepositoryScopeConditionCall struct { + *gomock.Call +} + +// Return rewrite *gomock.Call.Return +func (c *MockAdministratorRepositoryScopeConditionCall) Return(arg0 database.Condition) *MockAdministratorRepositoryScopeConditionCall { + c.Call = c.Call.Return(arg0) + return c +} + +// Do rewrite *gomock.Call.Do +func (c *MockAdministratorRepositoryScopeConditionCall) Do(f func(domain.AdministratorScope) database.Condition) *MockAdministratorRepositoryScopeConditionCall { + c.Call = c.Call.Do(f) + return c +} + +// DoAndReturn rewrite *gomock.Call.DoAndReturn +func (c *MockAdministratorRepositoryScopeConditionCall) DoAndReturn(f func(domain.AdministratorScope) database.Condition) *MockAdministratorRepositoryScopeConditionCall { + c.Call = c.Call.DoAndReturn(f) + return c +} + +// SetRoles mocks base method. +func (m *MockAdministratorRepository) SetRoles(roles []string) database.Change { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "SetRoles", roles) + ret0, _ := ret[0].(database.Change) + return ret0 +} + +// SetRoles indicates an expected call of SetRoles. +func (mr *MockAdministratorRepositoryMockRecorder) SetRoles(roles any) *MockAdministratorRepositorySetRolesCall { + mr.mock.ctrl.T.Helper() + call := mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "SetRoles", reflect.TypeOf((*MockAdministratorRepository)(nil).SetRoles), roles) + return &MockAdministratorRepositorySetRolesCall{Call: call} +} + +// MockAdministratorRepositorySetRolesCall wrap *gomock.Call +type MockAdministratorRepositorySetRolesCall struct { + *gomock.Call +} + +// Return rewrite *gomock.Call.Return +func (c *MockAdministratorRepositorySetRolesCall) Return(arg0 database.Change) *MockAdministratorRepositorySetRolesCall { + c.Call = c.Call.Return(arg0) + return c +} + +// Do rewrite *gomock.Call.Do +func (c *MockAdministratorRepositorySetRolesCall) Do(f func([]string) database.Change) *MockAdministratorRepositorySetRolesCall { + c.Call = c.Call.Do(f) + return c +} + +// DoAndReturn rewrite *gomock.Call.DoAndReturn +func (c *MockAdministratorRepositorySetRolesCall) DoAndReturn(f func([]string) database.Change) *MockAdministratorRepositorySetRolesCall { + c.Call = c.Call.DoAndReturn(f) + return c +} + +// SetUpdatedAt mocks base method. +func (m *MockAdministratorRepository) 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 *MockAdministratorRepositoryMockRecorder) SetUpdatedAt(updatedAt any) *MockAdministratorRepositorySetUpdatedAtCall { + mr.mock.ctrl.T.Helper() + call := mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "SetUpdatedAt", reflect.TypeOf((*MockAdministratorRepository)(nil).SetUpdatedAt), updatedAt) + return &MockAdministratorRepositorySetUpdatedAtCall{Call: call} +} + +// MockAdministratorRepositorySetUpdatedAtCall wrap *gomock.Call +type MockAdministratorRepositorySetUpdatedAtCall struct { + *gomock.Call +} + +// Return rewrite *gomock.Call.Return +func (c *MockAdministratorRepositorySetUpdatedAtCall) Return(arg0 database.Change) *MockAdministratorRepositorySetUpdatedAtCall { + c.Call = c.Call.Return(arg0) + return c +} + +// Do rewrite *gomock.Call.Do +func (c *MockAdministratorRepositorySetUpdatedAtCall) Do(f func(time.Time) database.Change) *MockAdministratorRepositorySetUpdatedAtCall { + c.Call = c.Call.Do(f) + return c +} + +// DoAndReturn rewrite *gomock.Call.DoAndReturn +func (c *MockAdministratorRepositorySetUpdatedAtCall) DoAndReturn(f func(time.Time) database.Change) *MockAdministratorRepositorySetUpdatedAtCall { + c.Call = c.Call.DoAndReturn(f) + return c +} + +// Update mocks base method. +func (m *MockAdministratorRepository) Update(ctx context.Context, client database.QueryExecutor, condition database.Condition, changes ...database.Change) (int64, error) { + m.ctrl.T.Helper() + varargs := []any{ctx, client, condition} + 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 *MockAdministratorRepositoryMockRecorder) Update(ctx, client, condition any, changes ...any) *MockAdministratorRepositoryUpdateCall { + mr.mock.ctrl.T.Helper() + varargs := append([]any{ctx, client, condition}, changes...) + call := mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Update", reflect.TypeOf((*MockAdministratorRepository)(nil).Update), varargs...) + return &MockAdministratorRepositoryUpdateCall{Call: call} +} + +// MockAdministratorRepositoryUpdateCall wrap *gomock.Call +type MockAdministratorRepositoryUpdateCall struct { + *gomock.Call +} + +// Return rewrite *gomock.Call.Return +func (c *MockAdministratorRepositoryUpdateCall) Return(arg0 int64, arg1 error) *MockAdministratorRepositoryUpdateCall { + c.Call = c.Call.Return(arg0, arg1) + return c +} + +// Do rewrite *gomock.Call.Do +func (c *MockAdministratorRepositoryUpdateCall) Do(f func(context.Context, database.QueryExecutor, database.Condition, ...database.Change) (int64, error)) *MockAdministratorRepositoryUpdateCall { + c.Call = c.Call.Do(f) + return c +} + +// DoAndReturn rewrite *gomock.Call.DoAndReturn +func (c *MockAdministratorRepositoryUpdateCall) DoAndReturn(f func(context.Context, database.QueryExecutor, database.Condition, ...database.Change) (int64, error)) *MockAdministratorRepositoryUpdateCall { + c.Call = c.Call.DoAndReturn(f) + return c +} + +// UpdatedAtColumn mocks base method. +func (m *MockAdministratorRepository) 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 *MockAdministratorRepositoryMockRecorder) UpdatedAtColumn() *MockAdministratorRepositoryUpdatedAtColumnCall { + mr.mock.ctrl.T.Helper() + call := mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "UpdatedAtColumn", reflect.TypeOf((*MockAdministratorRepository)(nil).UpdatedAtColumn)) + return &MockAdministratorRepositoryUpdatedAtColumnCall{Call: call} +} + +// MockAdministratorRepositoryUpdatedAtColumnCall wrap *gomock.Call +type MockAdministratorRepositoryUpdatedAtColumnCall struct { + *gomock.Call +} + +// Return rewrite *gomock.Call.Return +func (c *MockAdministratorRepositoryUpdatedAtColumnCall) Return(arg0 database.Column) *MockAdministratorRepositoryUpdatedAtColumnCall { + c.Call = c.Call.Return(arg0) + return c +} + +// Do rewrite *gomock.Call.Do +func (c *MockAdministratorRepositoryUpdatedAtColumnCall) Do(f func() database.Column) *MockAdministratorRepositoryUpdatedAtColumnCall { + c.Call = c.Call.Do(f) + return c +} + +// DoAndReturn rewrite *gomock.Call.DoAndReturn +func (c *MockAdministratorRepositoryUpdatedAtColumnCall) DoAndReturn(f func() database.Column) *MockAdministratorRepositoryUpdatedAtColumnCall { + c.Call = c.Call.DoAndReturn(f) + return c +} + +// UserIDColumn mocks base method. +func (m *MockAdministratorRepository) 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 *MockAdministratorRepositoryMockRecorder) UserIDColumn() *MockAdministratorRepositoryUserIDColumnCall { + mr.mock.ctrl.T.Helper() + call := mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "UserIDColumn", reflect.TypeOf((*MockAdministratorRepository)(nil).UserIDColumn)) + return &MockAdministratorRepositoryUserIDColumnCall{Call: call} +} + +// MockAdministratorRepositoryUserIDColumnCall wrap *gomock.Call +type MockAdministratorRepositoryUserIDColumnCall struct { + *gomock.Call +} + +// Return rewrite *gomock.Call.Return +func (c *MockAdministratorRepositoryUserIDColumnCall) Return(arg0 database.Column) *MockAdministratorRepositoryUserIDColumnCall { + c.Call = c.Call.Return(arg0) + return c +} + +// Do rewrite *gomock.Call.Do +func (c *MockAdministratorRepositoryUserIDColumnCall) Do(f func() database.Column) *MockAdministratorRepositoryUserIDColumnCall { + c.Call = c.Call.Do(f) + return c +} + +// DoAndReturn rewrite *gomock.Call.DoAndReturn +func (c *MockAdministratorRepositoryUserIDColumnCall) DoAndReturn(f func() database.Column) *MockAdministratorRepositoryUserIDColumnCall { + c.Call = c.Call.DoAndReturn(f) + return c +} + +// UserIDCondition mocks base method. +func (m *MockAdministratorRepository) 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 *MockAdministratorRepositoryMockRecorder) UserIDCondition(userID any) *MockAdministratorRepositoryUserIDConditionCall { + mr.mock.ctrl.T.Helper() + call := mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "UserIDCondition", reflect.TypeOf((*MockAdministratorRepository)(nil).UserIDCondition), userID) + return &MockAdministratorRepositoryUserIDConditionCall{Call: call} +} + +// MockAdministratorRepositoryUserIDConditionCall wrap *gomock.Call +type MockAdministratorRepositoryUserIDConditionCall struct { + *gomock.Call +} + +// Return rewrite *gomock.Call.Return +func (c *MockAdministratorRepositoryUserIDConditionCall) Return(arg0 database.Condition) *MockAdministratorRepositoryUserIDConditionCall { + c.Call = c.Call.Return(arg0) + return c +} + +// Do rewrite *gomock.Call.Do +func (c *MockAdministratorRepositoryUserIDConditionCall) Do(f func(string) database.Condition) *MockAdministratorRepositoryUserIDConditionCall { + c.Call = c.Call.Do(f) + return c +} + +// DoAndReturn rewrite *gomock.Call.DoAndReturn +func (c *MockAdministratorRepositoryUserIDConditionCall) DoAndReturn(f func(string) database.Condition) *MockAdministratorRepositoryUserIDConditionCall { + c.Call = c.Call.DoAndReturn(f) + return c +} diff --git a/backend/v3/domain/mock/org.mock.go b/backend/v3/domain/mock/org.mock.go index 35591af8c..cfcd1feba 100644 --- a/backend/v3/domain/mock/org.mock.go +++ b/backend/v3/domain/mock/org.mock.go @@ -12,6 +12,7 @@ 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" @@ -777,6 +778,44 @@ func (c *MockOrganizationRepositorySetStateCall) DoAndReturn(f func(domain.OrgSt return c } +// SetUpdatedAt mocks base method. +func (m *MockOrganizationRepository) 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 *MockOrganizationRepositoryMockRecorder) SetUpdatedAt(updatedAt any) *MockOrganizationRepositorySetUpdatedAtCall { + mr.mock.ctrl.T.Helper() + call := mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "SetUpdatedAt", reflect.TypeOf((*MockOrganizationRepository)(nil).SetUpdatedAt), updatedAt) + return &MockOrganizationRepositorySetUpdatedAtCall{Call: call} +} + +// MockOrganizationRepositorySetUpdatedAtCall wrap *gomock.Call +type MockOrganizationRepositorySetUpdatedAtCall struct { + *gomock.Call +} + +// Return rewrite *gomock.Call.Return +func (c *MockOrganizationRepositorySetUpdatedAtCall) Return(arg0 database.Change) *MockOrganizationRepositorySetUpdatedAtCall { + c.Call = c.Call.Return(arg0) + return c +} + +// Do rewrite *gomock.Call.Do +func (c *MockOrganizationRepositorySetUpdatedAtCall) Do(f func(time.Time) database.Change) *MockOrganizationRepositorySetUpdatedAtCall { + c.Call = c.Call.Do(f) + return c +} + +// DoAndReturn rewrite *gomock.Call.DoAndReturn +func (c *MockOrganizationRepositorySetUpdatedAtCall) DoAndReturn(f func(time.Time) database.Change) *MockOrganizationRepositorySetUpdatedAtCall { + c.Call = c.Call.DoAndReturn(f) + return c +} + // StateColumn mocks base method. func (m *MockOrganizationRepository) StateColumn() database.Column { m.ctrl.T.Helper() diff --git a/backend/v3/storage/database/dialect/postgres/migration/016_administrators_table.go b/backend/v3/storage/database/dialect/postgres/migration/016_administrators_table.go new file mode 100644 index 000000000..7e2f35156 --- /dev/null +++ b/backend/v3/storage/database/dialect/postgres/migration/016_administrators_table.go @@ -0,0 +1,16 @@ +package migration + +import ( + _ "embed" +) + +var ( + //go:embed 016_administrators_table/up.sql + up016AdministratorsTable string + //go:embed 016_administrators_table/down.sql + down016AdministratorsTable string +) + +func init() { + registerSQLMigration(16, up016AdministratorsTable, down016AdministratorsTable) +} diff --git a/backend/v3/storage/database/dialect/postgres/migration/016_administrators_table/down.sql b/backend/v3/storage/database/dialect/postgres/migration/016_administrators_table/down.sql new file mode 100644 index 000000000..a8507a60a --- /dev/null +++ b/backend/v3/storage/database/dialect/postgres/migration/016_administrators_table/down.sql @@ -0,0 +1,3 @@ +DROP TABLE IF EXISTS zitadel.administrator_roles; +DROP TABLE IF EXISTS zitadel.administrators; +DROP TYPE IF EXISTS zitadel.administrator_scope; diff --git a/backend/v3/storage/database/dialect/postgres/migration/016_administrators_table/up.sql b/backend/v3/storage/database/dialect/postgres/migration/016_administrators_table/up.sql new file mode 100644 index 000000000..4bb8ec5db --- /dev/null +++ b/backend/v3/storage/database/dialect/postgres/migration/016_administrators_table/up.sql @@ -0,0 +1,78 @@ +CREATE TYPE zitadel.administrator_scope AS ENUM ( + 'instance', + 'organization', + 'project', + 'project_grant' +); + +CREATE TABLE zitadel.administrators( + instance_id TEXT NOT NULL + , user_id TEXT NOT NULL + , scope zitadel.administrator_scope NOT NULL + + , created_at TIMESTAMPTZ NOT NULL DEFAULT NOW() + , updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW() + + , organization_id TEXT + , project_id TEXT + , project_grant_id TEXT + + , id TEXT GENERATED ALWAYS AS ( + user_id || ':' || + CASE scope + WHEN 'instance'::zitadel.administrator_scope + THEN 'instance:' || instance_id + WHEN 'organization'::zitadel.administrator_scope + THEN 'organization:' || organization_id + WHEN 'project'::zitadel.administrator_scope + THEN 'project:' || project_id + WHEN 'project_grant'::zitadel.administrator_scope + THEN 'project_grant:' || project_grant_id + END + ) STORED + + , CONSTRAINT administrators_scope_alignment_chk CHECK ( + (scope = 'instance' + AND organization_id IS NULL + AND project_id IS NULL + AND project_grant_id IS NULL) + OR + (scope = 'organization' + AND organization_id IS NOT NULL + AND project_id IS NULL + AND project_grant_id IS NULL) + OR + (scope = 'project' + AND organization_id IS NULL + AND project_id IS NOT NULL + AND project_grant_id IS NULL) + OR + (scope = 'project_grant' + AND organization_id IS NULL + AND project_id IS NULL + AND project_grant_id IS NOT NULL) + ) + + , PRIMARY KEY (instance_id, id) + , FOREIGN KEY (instance_id) REFERENCES zitadel.instances(id) ON DELETE CASCADE + , FOREIGN KEY (instance_id, user_id) REFERENCES zitadel.users(instance_id, id) ON DELETE CASCADE + , FOREIGN KEY (instance_id, organization_id) REFERENCES zitadel.organizations(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, project_grant_id) REFERENCES zitadel.project_grants(instance_id, id) ON DELETE CASCADE +); + +CREATE TABLE zitadel.administrator_roles( + instance_id TEXT NOT NULL + , administrator_id TEXT NOT NULL + , role_name TEXT NOT NULL CHECK (role_name <> '') + + , PRIMARY KEY (instance_id, administrator_id, role_name) + , FOREIGN KEY (instance_id, administrator_id) REFERENCES zitadel.administrators(instance_id, id) ON DELETE CASCADE +); + +CREATE TRIGGER trigger_set_updated_at + BEFORE UPDATE + ON zitadel.administrators + FOR EACH ROW + WHEN (NEW.updated_at IS NULL) +EXECUTE FUNCTION zitadel.set_updated_at(); diff --git a/backend/v3/storage/database/dialect/sql/rows.go b/backend/v3/storage/database/dialect/sql/rows.go index d4cb004f6..e5c0c1ebb 100644 --- a/backend/v3/storage/database/dialect/sql/rows.go +++ b/backend/v3/storage/database/dialect/sql/rows.go @@ -74,5 +74,5 @@ func (r *Rows) CollectExactlyOneRow(dest any) (err error) { // Close implements [database.Rows]. // Subtle: this method shadows the method (Rows).Close of Rows.Rows. func (r *Rows) Close() error { - return r.Rows.Close() + return wrapError(r.Rows.Close()) } diff --git a/backend/v3/storage/database/repository/administrator.go b/backend/v3/storage/database/repository/administrator.go new file mode 100644 index 000000000..c79f4630e --- /dev/null +++ b/backend/v3/storage/database/repository/administrator.go @@ -0,0 +1,378 @@ +package repository + +import ( + "context" + "slices" + "time" + + "github.com/zitadel/zitadel/backend/v3/domain" + "github.com/zitadel/zitadel/backend/v3/storage/database" + internaldb "github.com/zitadel/zitadel/internal/database" +) + +var _ domain.AdministratorRepository = (*administrator)(nil) + +type administrator struct{} + +func AdministratorRepository() domain.AdministratorRepository { + return new(administrator) +} + +func (a administrator) unqualifiedTableName() string { + return "administrators" +} + +func (a administrator) qualifiedTableName() string { + return "zitadel." + a.unqualifiedTableName() +} + +func (a administrator) unqualifiedRolesTableName() string { + return "administrator_roles" +} + +const queryAdministratorStmt = "SELECT administrators.instance_id " + + ", administrators.id " + + ", administrators.user_id " + + ", administrators.scope " + + ", administrators.organization_id " + + ", administrators.project_id " + + ", administrators.project_grant_id " + + ", administrators.created_at " + + ", administrators.updated_at " + + ", ARRAY_AGG(administrator_roles.role_name ORDER BY administrator_roles.role_name) FILTER (WHERE administrator_roles.administrator_id IS NOT NULL) AS roles " + + "FROM zitadel.administrators " + + "LEFT JOIN zitadel.administrator_roles" + + " ON administrators.instance_id = administrator_roles.instance_id" + + " AND administrators.id = administrator_roles.administrator_id" + +func (a administrator) Get(ctx context.Context, client database.QueryExecutor, opts ...database.QueryOption) (*domain.Administrator, error) { + builder, err := a.prepareQuery(opts) + if err != nil { + return nil, err + } + return scanAdministrator(ctx, client, builder) +} + +func (a administrator) List(ctx context.Context, client database.QueryExecutor, opts ...database.QueryOption) ([]*domain.Administrator, error) { + builder, err := a.prepareQuery(opts) + if err != nil { + return nil, err + } + return scanAdministrators(ctx, client, builder) +} + +func (a administrator) prepareQuery(opts []database.QueryOption) (*database.StatementBuilder, error) { + opts = append(opts, database.WithGroupBy(a.InstanceIDColumn(), a.IDColumn())) + 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(queryAdministratorStmt) + options.Write(builder) + return builder, nil +} + +func (a administrator) Create(ctx context.Context, client database.QueryExecutor, administrator *domain.Administrator) error { + var ( + createdAt any = database.DefaultInstruction + updatedAt any = database.DefaultInstruction + roles = rolesOrEmpty(administrator.Roles) + ) + if !administrator.CreatedAt.IsZero() { + createdAt = administrator.CreatedAt + } + if !administrator.UpdatedAt.IsZero() { + updatedAt = administrator.UpdatedAt + } + + builder := database.NewStatementBuilder("WITH inserted_admin AS (INSERT INTO zitadel.administrators (instance_id, user_id, scope, organization_id, project_id, project_grant_id, created_at, updated_at) VALUES (") + builder.WriteArgs( + administrator.InstanceID, + administrator.UserID, + administrator.Scope, + administrator.OrganizationID, + administrator.ProjectID, + administrator.ProjectGrantID, + createdAt, + updatedAt, + ) + builder.WriteString(") RETURNING id, instance_id, created_at, updated_at), inserted_roles AS (INSERT INTO zitadel.administrator_roles (instance_id, administrator_id, role_name) SELECT inserted_admin.instance_id, inserted_admin.id, unnest(") + builder.WriteArg(roles) + builder.WriteString("::TEXT[]) FROM inserted_admin) SELECT id, created_at, updated_at FROM inserted_admin") + + return client.QueryRow(ctx, builder.String(), builder.Args()...).Scan( + &administrator.ID, + &administrator.CreatedAt, + &administrator.UpdatedAt, + ) +} + +func (a administrator) Update(ctx context.Context, client database.QueryExecutor, condition database.Condition, changes ...database.Change) (int64, error) { + if len(changes) == 0 { + return 0, database.ErrNoChanges + } + if err := checkRestrictingColumns(condition, a.InstanceIDColumn(), a.UserIDColumn(), a.ScopeColumn()); err != nil { + return 0, err + } + + if !database.Changes(changes).IsOnColumn(a.UpdatedAtColumn()) { + changes = append(changes, a.clearUpdatedAt()) + } + + var builder database.StatementBuilder + builder.WriteString("WITH existing_administrator AS (SELECT * FROM zitadel.administrators ") + writeCondition(&builder, condition) + builder.WriteString(") ") + for i, change := range changes { + sessionCTE(change, i, 0, &builder) + } + builder.WriteString("UPDATE zitadel.administrators SET ") + if err := database.Changes(changes).Write(&builder); err != nil { + return 0, err + } + writeCondition(&builder, condition) + + return client.Exec(ctx, builder.String(), builder.Args()...) +} + +func (a administrator) Delete(ctx context.Context, client database.QueryExecutor, condition database.Condition) (int64, error) { + if err := checkRestrictingColumns(condition, a.InstanceIDColumn(), a.UserIDColumn(), a.ScopeColumn()); err != nil { + return 0, err + } + + builder := database.NewStatementBuilder("DELETE FROM ") + builder.WriteString(a.qualifiedTableName()) + builder.WriteRune(' ') + writeCondition(builder, condition) + + return client.Exec(ctx, builder.String(), builder.Args()...) +} + +func (a administrator) SetUpdatedAt(updatedAt time.Time) database.Change { + return database.NewChange(a.UpdatedAtColumn(), updatedAt) +} + +func (a administrator) AddRole(role string) database.Change { + return database.NewCTEChange( + func(builder *database.StatementBuilder) { + builder.WriteString("INSERT INTO zitadel.administrator_roles (instance_id, administrator_id, role_name) SELECT instance_id, id, ") + builder.WriteArg(role) + builder.WriteString(" FROM existing_administrator ON CONFLICT (instance_id, administrator_id, role_name) DO NOTHING") + }, nil, + ) +} + +func (a administrator) RemoveRole(role string) database.Change { + return database.NewCTEChange( + func(builder *database.StatementBuilder) { + builder.WriteString("DELETE FROM zitadel.administrator_roles ar USING existing_administrator ea WHERE ar.instance_id = ea.instance_id AND ar.administrator_id = ea.id AND ar.role_name = ") + builder.WriteArg(role) + }, nil, + ) +} + +func (a administrator) SetRoles(roles []string) database.Change { + normalizedRoles := rolesOrEmpty(roles) + return database.NewChanges( + database.NewCTEChange( + func(builder *database.StatementBuilder) { + builder.WriteString("INSERT INTO zitadel.administrator_roles (instance_id, administrator_id, role_name) SELECT instance_id, id, unnest(") + builder.WriteArg(normalizedRoles) + builder.WriteString("::text[]) FROM existing_administrator ON CONFLICT (instance_id, administrator_id, role_name) DO NOTHING") + }, nil, + ), + database.NewCTEChange( + func(builder *database.StatementBuilder) { + builder.WriteString("DELETE FROM zitadel.administrator_roles ar USING existing_administrator ea WHERE ar.instance_id = ea.instance_id AND ar.administrator_id = ea.id AND NOT ar.role_name = ANY (") + builder.WriteArg(normalizedRoles) + builder.WriteString("::text[])") + }, nil, + ), + ) +} + +func (a administrator) PrimaryKeyCondition(instanceID, administratorID string) database.Condition { + return database.And( + a.InstanceIDCondition(instanceID), + a.IDCondition(administratorID), + ) +} + +func (a administrator) IDCondition(administratorID string) database.Condition { + return database.NewTextCondition(a.IDColumn(), database.TextOperationEqual, administratorID) +} + +func (a administrator) InstanceIDCondition(instanceID string) database.Condition { + return database.NewTextCondition(a.InstanceIDColumn(), database.TextOperationEqual, instanceID) +} + +func (a administrator) UserIDCondition(userID string) database.Condition { + return database.NewTextCondition(a.UserIDColumn(), database.TextOperationEqual, userID) +} + +func (a administrator) ScopeCondition(scope domain.AdministratorScope) database.Condition { + return database.NewTextCondition(a.ScopeColumn(), database.TextOperationEqual, scope.String()) +} + +func (a administrator) InstanceAdministratorCondition(instanceID, userID string) database.Condition { + return database.And( + a.InstanceIDCondition(instanceID), + a.ScopeCondition(domain.AdministratorScopeInstance), + a.UserIDCondition(userID), + ) +} + +func (a administrator) OrganizationAdministratorCondition(instanceID, organizationID, userID string) database.Condition { + return database.And( + a.InstanceIDCondition(instanceID), + a.ScopeCondition(domain.AdministratorScopeOrganization), + a.OrganizationIDCondition(organizationID), + a.UserIDCondition(userID), + ) +} + +func (a administrator) ProjectAdministratorCondition(instanceID, projectID, userID string) database.Condition { + return database.And( + a.InstanceIDCondition(instanceID), + a.ScopeCondition(domain.AdministratorScopeProject), + a.ProjectIDCondition(projectID), + a.UserIDCondition(userID), + ) +} + +func (a administrator) ProjectGrantAdministratorCondition(instanceID, projectGrantID, userID string) database.Condition { + return database.And( + a.InstanceIDCondition(instanceID), + a.ScopeCondition(domain.AdministratorScopeProjectGrant), + a.ProjectGrantIDCondition(projectGrantID), + a.UserIDCondition(userID), + ) +} + +func (a administrator) OrganizationIDCondition(organizationID string) database.Condition { + return database.NewTextCondition(a.OrganizationIDColumn(), database.TextOperationEqual, organizationID) +} + +func (a administrator) ProjectIDCondition(projectID string) database.Condition { + return database.NewTextCondition(a.ProjectIDColumn(), database.TextOperationEqual, projectID) +} + +func (a administrator) ProjectGrantIDCondition(projectGrantID string) database.Condition { + return database.NewTextCondition(a.ProjectGrantIDColumn(), database.TextOperationEqual, projectGrantID) +} + +func (a administrator) RoleCondition(op database.TextOperation, role string) database.Condition { + return database.NewTextCondition(database.NewColumn(a.unqualifiedRolesTableName(), "role_name"), op, role) +} + +func (a administrator) PrimaryKeyColumns() []database.Column { + return []database.Column{ + a.InstanceIDColumn(), + a.IDColumn(), + } +} + +func (a administrator) IDColumn() database.Column { + return database.NewColumn(a.unqualifiedTableName(), "id") +} + +func (a administrator) InstanceIDColumn() database.Column { + return database.NewColumn(a.unqualifiedTableName(), "instance_id") +} + +func (a administrator) UserIDColumn() database.Column { + return database.NewColumn(a.unqualifiedTableName(), "user_id") +} + +func (a administrator) ScopeColumn() database.Column { + return database.NewColumn(a.unqualifiedTableName(), "scope") +} + +func (a administrator) OrganizationIDColumn() database.Column { + return database.NewColumn(a.unqualifiedTableName(), "organization_id") +} + +func (a administrator) ProjectIDColumn() database.Column { + return database.NewColumn(a.unqualifiedTableName(), "project_id") +} + +func (a administrator) ProjectGrantIDColumn() database.Column { + return database.NewColumn(a.unqualifiedTableName(), "project_grant_id") +} + +func (a administrator) CreatedAtColumn() database.Column { + return database.NewColumn(a.unqualifiedTableName(), "created_at") +} + +func (a administrator) UpdatedAtColumn() database.Column { + return database.NewColumn(a.unqualifiedTableName(), "updated_at") +} + +func (a administrator) clearUpdatedAt() database.Change { + return database.NewChange(a.UpdatedAtColumn(), database.NullInstruction) +} + +// rawAdministrator is used for query collection because ARRAY_AGG roles cannot be +// scanned directly into the domain model's []string field when the client is backed +// by [database/sql]. The intermediate TextArray field works with both [database/sql] and pgx. +type rawAdministrator struct { + domain.Administrator + Roles internaldb.TextArray[string] `db:"roles"` +} + +func (a *rawAdministrator) toDomain() *domain.Administrator { + a.Administrator.Roles = []string(a.Roles) + return &a.Administrator +} + +func scanAdministrator(ctx context.Context, client database.QueryExecutor, builder *database.StatementBuilder) (*domain.Administrator, error) { + rows, err := client.Query(ctx, builder.String(), builder.Args()...) + if err != nil { + return nil, err + } + + var administrator rawAdministrator + if err = rows.(database.CollectableRows).CollectExactlyOneRow(&administrator); err != nil { + return nil, err + } + return administrator.toDomain(), nil +} + +func scanAdministrators(ctx context.Context, client database.QueryExecutor, builder *database.StatementBuilder) ([]*domain.Administrator, error) { + rows, err := client.Query(ctx, builder.String(), builder.Args()...) + if err != nil { + return nil, err + } + + var administrators []rawAdministrator + if err = rows.(database.CollectableRows).Collect(&administrators); err != nil { + return nil, err + } + + result := make([]*domain.Administrator, len(administrators)) + for i := range administrators { + result[i] = administrators[i].toDomain() + } + return result, nil +} + +func uniqueStrings(values []string) []string { + result := make([]string, 0, len(values)) + for _, value := range values { + if slices.Contains(result, value) { + continue + } + result = append(result, value) + } + return result +} + +func rolesOrEmpty(roles []string) []string { + if roles == nil { + return []string{} + } + return uniqueStrings(roles) +} diff --git a/backend/v3/storage/database/repository/administrator_test.go b/backend/v3/storage/database/repository/administrator_test.go new file mode 100644 index 000000000..e5965f7d3 --- /dev/null +++ b/backend/v3/storage/database/repository/administrator_test.go @@ -0,0 +1,665 @@ +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 TestAdministratorRepository_Create(t *testing.T) { + tx, rollback := transactionForRollback(t) + defer rollback() + + instanceID := createInstance(t, tx) + orgID := createOrganization(t, tx, instanceID) + projectID := createProject(t, tx, instanceID, orgID) + grantedOrgID := createOrganization(t, tx, instanceID) + projectGrantID := createProjectGrant(t, tx, instanceID, orgID, grantedOrgID, projectID, nil) + userID := createHumanUser(t, tx, instanceID, orgID) + adminRepo := repository.AdministratorRepository() + + // Pre-create an admin for duplicate detection. + duplicateUser := createHumanUser(t, tx, instanceID, orgID) + err := adminRepo.Create(t.Context(), tx, &domain.Administrator{ + InstanceID: instanceID, + UserID: duplicateUser, + Scope: domain.AdministratorScopeOrganization, + OrganizationID: gu.Ptr(orgID), + Roles: []string{"ORG_OWNER"}, + }) + require.NoError(t, err) + + tests := []struct { + name string + administrator *domain.Administrator + wantErr error + }{ + { + name: "instance scope", + administrator: &domain.Administrator{ + InstanceID: instanceID, + UserID: userID, + Scope: domain.AdministratorScopeInstance, + Roles: []string{"IAM_OWNER"}, + }, + }, + { + name: "organization scope", + administrator: &domain.Administrator{ + InstanceID: instanceID, + UserID: createHumanUser(t, tx, instanceID, orgID), + Scope: domain.AdministratorScopeOrganization, + OrganizationID: gu.Ptr(orgID), + Roles: []string{"ORG_OWNER"}, + }, + }, + { + name: "project scope", + administrator: &domain.Administrator{ + InstanceID: instanceID, + UserID: userID, + Scope: domain.AdministratorScopeProject, + ProjectID: gu.Ptr(projectID), + Roles: []string{"PROJECT_OWNER"}, + }, + }, + { + name: "project grant scope", + administrator: &domain.Administrator{ + InstanceID: instanceID, + UserID: createHumanUser(t, tx, instanceID, orgID), + Scope: domain.AdministratorScopeProjectGrant, + ProjectGrantID: gu.Ptr(projectGrantID), + Roles: []string{"PROJECT_OWNER"}, + }, + }, + { + name: "duplicate admin", + administrator: &domain.Administrator{ + InstanceID: instanceID, + UserID: duplicateUser, + Scope: domain.AdministratorScopeOrganization, + OrganizationID: gu.Ptr(orgID), + Roles: []string{"ORG_OWNER"}, + }, + wantErr: new(database.UniqueError), + }, + { + name: "missing user", + administrator: &domain.Administrator{ + InstanceID: instanceID, + UserID: integration.ID(), + Scope: domain.AdministratorScopeOrganization, + OrganizationID: gu.Ptr(orgID), + Roles: []string{"ORG_OWNER"}, + }, + wantErr: new(database.ForeignKeyError), + }, + { + name: "missing organization", + administrator: &domain.Administrator{ + InstanceID: instanceID, + UserID: createHumanUser(t, tx, instanceID, orgID), + Scope: domain.AdministratorScopeOrganization, + OrganizationID: gu.Ptr(integration.ID()), + Roles: []string{"ORG_OWNER"}, + }, + wantErr: new(database.ForeignKeyError), + }, + { + name: "missing project", + administrator: &domain.Administrator{ + InstanceID: instanceID, + UserID: userID, + Scope: domain.AdministratorScopeProject, + ProjectID: gu.Ptr(integration.ID()), + Roles: []string{"PROJECT_OWNER"}, + }, + wantErr: new(database.ForeignKeyError), + }, + { + name: "missing project grant", + administrator: &domain.Administrator{ + InstanceID: instanceID, + UserID: createHumanUser(t, tx, instanceID, orgID), + Scope: domain.AdministratorScopeProjectGrant, + ProjectGrantID: gu.Ptr(integration.ID()), + Roles: []string{"PROJECT_OWNER"}, + }, + wantErr: new(database.ForeignKeyError), + }, + { + name: "invalid scope alignment", + administrator: &domain.Administrator{ + InstanceID: instanceID, + UserID: createHumanUser(t, tx, instanceID, orgID), + Scope: domain.AdministratorScopeProjectGrant, + ProjectID: gu.Ptr(projectID), + ProjectGrantID: gu.Ptr(projectGrantID), + Roles: []string{"PROJECT_OWNER"}, + }, + wantErr: new(database.CheckError), + }, + { + name: "empty role", + administrator: &domain.Administrator{ + InstanceID: instanceID, + UserID: createHumanUser(t, tx, instanceID, orgID), + Scope: domain.AdministratorScopeOrganization, + OrganizationID: gu.Ptr(orgID), + Roles: []string{""}, + }, + wantErr: new(database.CheckError), + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + savepoint, rollback := savepointForRollback(t, tx) + defer rollback() + + err := adminRepo.Create(t.Context(), savepoint, tt.administrator) + if tt.wantErr != nil { + require.ErrorIs(t, err, tt.wantErr) + return + } + require.NoError(t, err) + assert.NotEmpty(t, tt.administrator.ID) + assert.NotZero(t, tt.administrator.CreatedAt) + assert.NotZero(t, tt.administrator.UpdatedAt) + + got, err := adminRepo.Get(t.Context(), savepoint, database.WithCondition( + adminRepo.PrimaryKeyCondition(instanceID, tt.administrator.ID), + )) + require.NoError(t, err) + assert.Equal(t, tt.administrator.InstanceID, got.InstanceID) + assert.Equal(t, tt.administrator.UserID, got.UserID) + assert.Equal(t, tt.administrator.Scope, got.Scope) + assert.Equal(t, tt.administrator.OrganizationID, got.OrganizationID) + assert.Equal(t, tt.administrator.ProjectID, got.ProjectID) + assert.Equal(t, tt.administrator.ProjectGrantID, got.ProjectGrantID) + assert.ElementsMatch(t, tt.administrator.Roles, got.Roles) + }) + } +} + +func TestAdministratorRepository_Get(t *testing.T) { + tx, rollback := transactionForRollback(t) + defer rollback() + + instanceID := createInstance(t, tx) + orgID := createOrganization(t, tx, instanceID) + projectID := createProject(t, tx, instanceID, orgID) + grantedOrgID := createOrganization(t, tx, instanceID) + projectGrantID := createProjectGrant(t, tx, instanceID, orgID, grantedOrgID, projectID, nil) + userID := createHumanUser(t, tx, instanceID, orgID) + adminRepo := repository.AdministratorRepository() + + instanceAdmin := &domain.Administrator{ + InstanceID: instanceID, + UserID: userID, + Scope: domain.AdministratorScopeInstance, + Roles: []string{"IAM_OWNER"}, + } + require.NoError(t, adminRepo.Create(t.Context(), tx, instanceAdmin)) + + orgAdmin := &domain.Administrator{ + InstanceID: instanceID, + UserID: userID, + Scope: domain.AdministratorScopeOrganization, + OrganizationID: gu.Ptr(orgID), + Roles: []string{"ORG_OWNER"}, + } + require.NoError(t, adminRepo.Create(t.Context(), tx, orgAdmin)) + + projectAdmin := &domain.Administrator{ + InstanceID: instanceID, + UserID: userID, + Scope: domain.AdministratorScopeProject, + ProjectID: gu.Ptr(projectID), + Roles: []string{"PROJECT_OWNER"}, + } + require.NoError(t, adminRepo.Create(t.Context(), tx, projectAdmin)) + + projectGrantAdmin := &domain.Administrator{ + InstanceID: instanceID, + UserID: userID, + Scope: domain.AdministratorScopeProjectGrant, + ProjectGrantID: gu.Ptr(projectGrantID), + Roles: []string{"PROJECT_OWNER"}, + } + require.NoError(t, adminRepo.Create(t.Context(), tx, projectGrantAdmin)) + + tests := []struct { + name string + condition database.Condition + wantAdmin *domain.Administrator + wantErr error + }{ + { + name: "instance scope", + condition: adminRepo.InstanceAdministratorCondition(instanceID, userID), + wantAdmin: instanceAdmin, + }, + { + name: "organization scope", + condition: adminRepo.OrganizationAdministratorCondition(instanceID, orgID, userID), + wantAdmin: orgAdmin, + }, + { + name: "project scope", + condition: adminRepo.ProjectAdministratorCondition(instanceID, projectID, userID), + wantAdmin: projectAdmin, + }, + { + name: "project grant scope", + condition: adminRepo.ProjectGrantAdministratorCondition(instanceID, projectGrantID, userID), + wantAdmin: projectGrantAdmin, + }, + { + name: "by primary key", + condition: adminRepo.PrimaryKeyCondition(instanceID, orgAdmin.ID), + wantAdmin: orgAdmin, + }, + { + name: "not found", + condition: adminRepo.InstanceAdministratorCondition(instanceID, integration.ID()), + wantErr: database.NewNoRowFoundError(nil), + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + savepoint, rollback := savepointForRollback(t, tx) + defer rollback() + + got, err := adminRepo.Get(t.Context(), savepoint, database.WithCondition(tt.condition)) + if tt.wantErr != nil { + require.ErrorIs(t, err, tt.wantErr) + return + } + require.NoError(t, err) + assert.Equal(t, tt.wantAdmin.ID, got.ID) + assert.Equal(t, tt.wantAdmin.Scope, got.Scope) + assert.Equal(t, tt.wantAdmin.UserID, got.UserID) + assert.Equal(t, tt.wantAdmin.OrganizationID, got.OrganizationID) + assert.Equal(t, tt.wantAdmin.ProjectID, got.ProjectID) + assert.Equal(t, tt.wantAdmin.ProjectGrantID, got.ProjectGrantID) + assert.ElementsMatch(t, tt.wantAdmin.Roles, got.Roles) + }) + } +} + +func TestAdministratorRepository_List(t *testing.T) { + tx, rollback := transactionForRollback(t) + defer rollback() + + instanceID := createInstance(t, tx) + orgID := createOrganization(t, tx, instanceID) + otherOrgID := createOrganization(t, tx, instanceID) + projectID := createProject(t, tx, instanceID, orgID) + grantedOrgID := createOrganization(t, tx, instanceID) + projectGrantID := createProjectGrant(t, tx, instanceID, orgID, grantedOrgID, projectID, nil) + user1 := createHumanUser(t, tx, instanceID, orgID) + user2 := createHumanUser(t, tx, instanceID, orgID) + adminRepo := repository.AdministratorRepository() + + // instance: user1=IAM_OWNER, user2=IAM_VIEWER + require.NoError(t, adminRepo.Create(t.Context(), tx, &domain.Administrator{ + InstanceID: instanceID, UserID: user1, + Scope: domain.AdministratorScopeInstance, Roles: []string{"IAM_OWNER"}, + })) + require.NoError(t, adminRepo.Create(t.Context(), tx, &domain.Administrator{ + InstanceID: instanceID, UserID: user2, + Scope: domain.AdministratorScopeInstance, Roles: []string{"IAM_VIEWER"}, + })) + // organization: user1 in orgID + require.NoError(t, adminRepo.Create(t.Context(), tx, &domain.Administrator{ + InstanceID: instanceID, UserID: user1, + Scope: domain.AdministratorScopeOrganization, OrganizationID: gu.Ptr(orgID), + Roles: []string{"ORG_OWNER"}, + })) + // project: user1 + require.NoError(t, adminRepo.Create(t.Context(), tx, &domain.Administrator{ + InstanceID: instanceID, UserID: user1, + Scope: domain.AdministratorScopeProject, ProjectID: gu.Ptr(projectID), + Roles: []string{"PROJECT_OWNER"}, + })) + // project grant: user1 + require.NoError(t, adminRepo.Create(t.Context(), tx, &domain.Administrator{ + InstanceID: instanceID, UserID: user1, + Scope: domain.AdministratorScopeProjectGrant, ProjectGrantID: gu.Ptr(projectGrantID), + Roles: []string{"PROJECT_OWNER"}, + })) + + tests := []struct { + name string + condition database.Condition + wantLen int + }{ + { + name: "all instance admins", + condition: database.And( + adminRepo.InstanceIDCondition(instanceID), + adminRepo.ScopeCondition(domain.AdministratorScopeInstance), + ), + wantLen: 2, + }, + { + name: "instance admins filtered by role", + condition: database.And( + adminRepo.InstanceIDCondition(instanceID), + adminRepo.ScopeCondition(domain.AdministratorScopeInstance), + adminRepo.RoleCondition(database.TextOperationEqual, "IAM_OWNER"), + ), + wantLen: 1, + }, + { + name: "org admins in org", + condition: database.And( + adminRepo.InstanceIDCondition(instanceID), + adminRepo.ScopeCondition(domain.AdministratorScopeOrganization), + adminRepo.OrganizationIDCondition(orgID), + ), + wantLen: 1, + }, + { + name: "org admins in other org returns empty", + condition: database.And( + adminRepo.InstanceIDCondition(instanceID), + adminRepo.ScopeCondition(domain.AdministratorScopeOrganization), + adminRepo.OrganizationIDCondition(otherOrgID), + ), + wantLen: 0, + }, + { + name: "project admins", + condition: database.And( + adminRepo.InstanceIDCondition(instanceID), + adminRepo.ScopeCondition(domain.AdministratorScopeProject), + adminRepo.ProjectIDCondition(projectID), + ), + wantLen: 1, + }, + { + name: "project grant admins", + condition: database.And( + adminRepo.InstanceIDCondition(instanceID), + adminRepo.ScopeCondition(domain.AdministratorScopeProjectGrant), + adminRepo.ProjectGrantIDCondition(projectGrantID), + ), + wantLen: 1, + }, + { + name: "scope isolation: instance condition excludes org admin", + condition: adminRepo.InstanceAdministratorCondition(instanceID, user1), + wantLen: 1, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + savepoint, rollback := savepointForRollback(t, tx) + defer rollback() + + list, err := adminRepo.List(t.Context(), savepoint, database.WithCondition(tt.condition)) + require.NoError(t, err) + assert.Len(t, list, tt.wantLen) + }) + } +} + +func TestAdministratorRepository_Update(t *testing.T) { + tx, rollback := transactionForRollback(t) + defer rollback() + + instanceID := createInstance(t, tx) + orgID := createOrganization(t, tx, instanceID) + userID := createHumanUser(t, tx, instanceID, orgID) + adminRepo := repository.AdministratorRepository() + + admin := &domain.Administrator{ + InstanceID: instanceID, + UserID: userID, + Scope: domain.AdministratorScopeOrganization, + OrganizationID: gu.Ptr(orgID), + Roles: []string{"ORG_OWNER"}, + } + require.NoError(t, adminRepo.Create(t.Context(), tx, admin)) + originalCreatedAt := admin.CreatedAt + + condition := adminRepo.OrganizationAdministratorCondition(instanceID, orgID, userID) + + tests := []struct { + name string + changes []database.Change + verify func(t *testing.T, got *domain.Administrator) + }{ + { + name: "SetUpdatedAt", + changes: []database.Change{adminRepo.SetUpdatedAt(time.Date(2025, 6, 15, 12, 0, 0, 0, time.UTC))}, + verify: func(t *testing.T, got *domain.Administrator) { + assert.Equal(t, time.Date(2025, 6, 15, 12, 0, 0, 0, time.UTC), got.UpdatedAt.UTC()) + assert.Equal(t, []string{"ORG_OWNER"}, got.Roles) + assert.Equal(t, originalCreatedAt, got.CreatedAt, "created_at must not change") + }, + }, + { + name: "SetRoles replaces all", + changes: []database.Change{adminRepo.SetRoles([]string{"AUDITOR", "VIEWER"})}, + verify: func(t *testing.T, got *domain.Administrator) { + assert.ElementsMatch(t, []string{"AUDITOR", "VIEWER"}, got.Roles) + }, + }, + { + name: "SetRoles to single role", + changes: []database.Change{adminRepo.SetRoles([]string{"AUDITOR"})}, + verify: func(t *testing.T, got *domain.Administrator) { + assert.Equal(t, []string{"AUDITOR"}, got.Roles) + }, + }, + { + name: "SetRoles to empty removes all", + changes: []database.Change{adminRepo.SetRoles([]string{})}, + verify: func(t *testing.T, got *domain.Administrator) { + assert.Empty(t, got.Roles) + }, + }, + { + name: "AddRole new", + changes: []database.Change{adminRepo.AddRole("AUDITOR")}, + verify: func(t *testing.T, got *domain.Administrator) { + assert.ElementsMatch(t, []string{"ORG_OWNER", "AUDITOR"}, got.Roles) + }, + }, + { + name: "AddRole existing is idempotent", + changes: []database.Change{adminRepo.AddRole("ORG_OWNER")}, + verify: func(t *testing.T, got *domain.Administrator) { + assert.Equal(t, []string{"ORG_OWNER"}, got.Roles) + }, + }, + { + name: "RemoveRole existing", + changes: []database.Change{adminRepo.RemoveRole("ORG_OWNER")}, + verify: func(t *testing.T, got *domain.Administrator) { + assert.Empty(t, got.Roles) + }, + }, + { + name: "RemoveRole absent is idempotent", + changes: []database.Change{adminRepo.RemoveRole("NONEXISTENT")}, + verify: func(t *testing.T, got *domain.Administrator) { + assert.Equal(t, []string{"ORG_OWNER"}, got.Roles) + }, + }, + { + name: "SetUpdatedAt and SetRoles combined", + changes: []database.Change{ + adminRepo.SetUpdatedAt(time.Date(2025, 6, 15, 12, 0, 0, 0, time.UTC)), + adminRepo.SetRoles([]string{"AUDITOR"}), + }, + verify: func(t *testing.T, got *domain.Administrator) { + assert.Equal(t, time.Date(2025, 6, 15, 12, 0, 0, 0, time.UTC), got.UpdatedAt.UTC()) + assert.Equal(t, []string{"AUDITOR"}, got.Roles) + }, + }, + { + name: "AddRole and RemoveRole combined", + changes: []database.Change{ + adminRepo.AddRole("AUDITOR"), + adminRepo.RemoveRole("ORG_OWNER"), + }, + verify: func(t *testing.T, got *domain.Administrator) { + assert.Equal(t, []string{"AUDITOR"}, got.Roles) + }, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + savepoint, rollback := savepointForRollback(t, tx) + defer rollback() + + _, err := adminRepo.Update(t.Context(), savepoint, condition, tt.changes...) + require.NoError(t, err) + + got, err := adminRepo.Get(t.Context(), savepoint, database.WithCondition(condition)) + require.NoError(t, err) + tt.verify(t, got) + }) + } +} + +func TestAdministratorRepository_Delete(t *testing.T) { + tx, rollback := transactionForRollback(t) + defer rollback() + + instanceID := createInstance(t, tx) + orgID := createOrganization(t, tx, instanceID) + projectID := createProject(t, tx, instanceID, orgID) + grantedOrgID := createOrganization(t, tx, instanceID) + projectGrantID := createProjectGrant(t, tx, instanceID, orgID, grantedOrgID, projectID, nil) + user1 := createHumanUser(t, tx, instanceID, orgID) + user2 := createHumanUser(t, tx, instanceID, orgID) + adminRepo := repository.AdministratorRepository() + + instanceAdmin := &domain.Administrator{ + InstanceID: instanceID, UserID: user1, + Scope: domain.AdministratorScopeInstance, Roles: []string{"IAM_OWNER"}, + } + require.NoError(t, adminRepo.Create(t.Context(), tx, instanceAdmin)) + + otherInstanceAdmin := &domain.Administrator{ + InstanceID: instanceID, UserID: user2, + Scope: domain.AdministratorScopeInstance, Roles: []string{"IAM_VIEWER"}, + } + require.NoError(t, adminRepo.Create(t.Context(), tx, otherInstanceAdmin)) + + orgAdmin := &domain.Administrator{ + InstanceID: instanceID, UserID: user1, + Scope: domain.AdministratorScopeOrganization, OrganizationID: gu.Ptr(orgID), + Roles: []string{"ORG_OWNER"}, + } + require.NoError(t, adminRepo.Create(t.Context(), tx, orgAdmin)) + + projectAdmin := &domain.Administrator{ + InstanceID: instanceID, UserID: user1, + Scope: domain.AdministratorScopeProject, ProjectID: gu.Ptr(projectID), + Roles: []string{"PROJECT_OWNER"}, + } + require.NoError(t, adminRepo.Create(t.Context(), tx, projectAdmin)) + + projectGrantAdmin := &domain.Administrator{ + InstanceID: instanceID, UserID: user1, + Scope: domain.AdministratorScopeProjectGrant, ProjectGrantID: gu.Ptr(projectGrantID), + Roles: []string{"PROJECT_OWNER"}, + } + require.NoError(t, adminRepo.Create(t.Context(), tx, projectGrantAdmin)) + + tests := []struct { + name string + condition database.Condition + verifyRemaining func(t *testing.T, sp database.Transaction) + }{ + { + name: "instance admin, other scopes remain", + condition: adminRepo.InstanceAdministratorCondition(instanceID, user1), + verifyRemaining: func(t *testing.T, sp database.Transaction) { + _, err := adminRepo.Get(t.Context(), sp, database.WithCondition( + adminRepo.InstanceAdministratorCondition(instanceID, user2), + )) + require.NoError(t, err) + _, err = adminRepo.Get(t.Context(), sp, database.WithCondition( + adminRepo.OrganizationAdministratorCondition(instanceID, orgID, user1), + )) + require.NoError(t, err) + }, + }, + { + name: "organization admin, other scopes remain", + condition: adminRepo.OrganizationAdministratorCondition(instanceID, orgID, user1), + verifyRemaining: func(t *testing.T, sp database.Transaction) { + _, err := adminRepo.Get(t.Context(), sp, database.WithCondition( + adminRepo.InstanceAdministratorCondition(instanceID, user1), + )) + require.NoError(t, err) + _, err = adminRepo.Get(t.Context(), sp, database.WithCondition( + adminRepo.ProjectAdministratorCondition(instanceID, projectID, user1), + )) + require.NoError(t, err) + }, + }, + { + name: "project admin, other scopes remain", + condition: adminRepo.ProjectAdministratorCondition(instanceID, projectID, user1), + verifyRemaining: func(t *testing.T, sp database.Transaction) { + _, err := adminRepo.Get(t.Context(), sp, database.WithCondition( + adminRepo.InstanceAdministratorCondition(instanceID, user1), + )) + require.NoError(t, err) + _, err = adminRepo.Get(t.Context(), sp, database.WithCondition( + adminRepo.ProjectGrantAdministratorCondition(instanceID, projectGrantID, user1), + )) + require.NoError(t, err) + }, + }, + { + name: "project grant admin, other scopes remain", + condition: adminRepo.ProjectGrantAdministratorCondition(instanceID, projectGrantID, user1), + verifyRemaining: func(t *testing.T, sp database.Transaction) { + _, err := adminRepo.Get(t.Context(), sp, database.WithCondition( + adminRepo.InstanceAdministratorCondition(instanceID, user1), + )) + require.NoError(t, err) + _, err = adminRepo.Get(t.Context(), sp, database.WithCondition( + adminRepo.OrganizationAdministratorCondition(instanceID, orgID, user1), + )) + require.NoError(t, err) + }, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + savepoint, rollback := savepointForRollback(t, tx) + defer rollback() + + _, err := adminRepo.Delete(t.Context(), savepoint, tt.condition) + require.NoError(t, err) + + _, err = adminRepo.Get(t.Context(), savepoint, database.WithCondition(tt.condition)) + require.ErrorIs(t, err, database.NewNoRowFoundError(nil)) + + tt.verifyRemaining(t, savepoint) + }) + } +} diff --git a/backend/v3/storage/database/repository/authorization.go b/backend/v3/storage/database/repository/authorization.go index c309a913f..bf6a623be 100644 --- a/backend/v3/storage/database/repository/authorization.go +++ b/backend/v3/storage/database/repository/authorization.go @@ -6,6 +6,7 @@ import ( "github.com/zitadel/zitadel/backend/v3/domain" "github.com/zitadel/zitadel/backend/v3/storage/database" + internaldb "github.com/zitadel/zitadel/internal/database" ) var _ domain.AuthorizationRepository = (*authorization)(nil) @@ -93,7 +94,7 @@ func (a authorization) Get(ctx context.Context, client database.QueryExecutor, o if err != nil { return nil, err } - return getOne[domain.Authorization](ctx, client, builder) + return scanAuthorization(ctx, client, builder) } // List implements [domain.AuthorizationRepository]. @@ -106,7 +107,7 @@ func (a authorization) List(ctx context.Context, client database.QueryExecutor, if err != nil { return nil, err } - return getMany[domain.Authorization](ctx, client, builder) + return scanAuthorizations(ctx, client, builder) } func (a authorization) prepareQuery(opts []database.QueryOption) (*database.StatementBuilder, error) { @@ -196,6 +197,50 @@ func (a authorization) Delete(ctx context.Context, client database.QueryExecutor return client.Exec(ctx, builder.String(), builder.Args()...) } +// rawAuthorization is used for query collection because ARRAY_AGG roles cannot be +// scanned directly into the domain model's []string field when the client is backed +// by [database/sql]. The intermediate TextArray field works with both [database/sql] and pgx. +type rawAuthorization struct { + domain.Authorization + Roles internaldb.TextArray[string] `db:"roles"` +} + +func (a *rawAuthorization) toDomain() *domain.Authorization { + a.Authorization.Roles = []string(a.Roles) + return &a.Authorization +} + +func scanAuthorization(ctx context.Context, client database.QueryExecutor, builder *database.StatementBuilder) (*domain.Authorization, error) { + rows, err := client.Query(ctx, builder.String(), builder.Args()...) + if err != nil { + return nil, err + } + + var authorization rawAuthorization + if err = rows.(database.CollectableRows).CollectExactlyOneRow(&authorization); err != nil { + return nil, err + } + return authorization.toDomain(), nil +} + +func scanAuthorizations(ctx context.Context, client database.QueryExecutor, builder *database.StatementBuilder) ([]*domain.Authorization, error) { + rows, err := client.Query(ctx, builder.String(), builder.Args()...) + if err != nil { + return nil, err + } + + var authorizations []rawAuthorization + if err = rows.(database.CollectableRows).Collect(&authorizations); err != nil { + return nil, err + } + + result := make([]*domain.Authorization, len(authorizations)) + for i := range authorizations { + result[i] = authorizations[i].toDomain() + } + return result, nil +} + // ------------------------------------------------------------- // columns // ------------------------------------------------------------- diff --git a/backend/v3/storage/database/repository/project_grant.go b/backend/v3/storage/database/repository/project_grant.go index ff9638b93..7e606526d 100644 --- a/backend/v3/storage/database/repository/project_grant.go +++ b/backend/v3/storage/database/repository/project_grant.go @@ -6,6 +6,7 @@ import ( "github.com/zitadel/zitadel/backend/v3/domain" "github.com/zitadel/zitadel/backend/v3/storage/database" + internaldb "github.com/zitadel/zitadel/internal/database" ) // ------------------------------------------------------------- @@ -28,7 +29,7 @@ func (p projectGrant) Get(ctx context.Context, client database.QueryExecutor, op if err != nil { return nil, err } - return getOne[domain.ProjectGrant](ctx, client, builder) + return scanProjectGrant(ctx, client, builder) } func (p projectGrant) List(ctx context.Context, client database.QueryExecutor, opts ...database.QueryOption) ([]*domain.ProjectGrant, error) { @@ -40,7 +41,7 @@ func (p projectGrant) List(ctx context.Context, client database.QueryExecutor, o if err != nil { return nil, err } - return getMany[domain.ProjectGrant](ctx, client, builder) + return scanProjectGrants(ctx, client, builder) } const insertProjectGrantRolesStmt = `WITH added_roles AS ( @@ -307,3 +308,47 @@ func (p projectGrant) prepareQuery(opts []database.QueryOption) (*database.State return builder, nil } + +// rawProjectGrant is used for query collection because ARRAY_AGG role keys cannot be +// scanned directly into the domain model's []string field when the client is backed +// by [database/sql]. The intermediate TextArray field works with both [database/sql] and pgx. +type rawProjectGrant struct { + domain.ProjectGrant + RoleKeys internaldb.TextArray[string] `db:"role_keys"` +} + +func (p *rawProjectGrant) toDomain() *domain.ProjectGrant { + p.ProjectGrant.RoleKeys = []string(p.RoleKeys) + return &p.ProjectGrant +} + +func scanProjectGrant(ctx context.Context, client database.QueryExecutor, builder *database.StatementBuilder) (*domain.ProjectGrant, error) { + rows, err := client.Query(ctx, builder.String(), builder.Args()...) + if err != nil { + return nil, err + } + + var projectGrant rawProjectGrant + if err = rows.(database.CollectableRows).CollectExactlyOneRow(&projectGrant); err != nil { + return nil, err + } + return projectGrant.toDomain(), nil +} + +func scanProjectGrants(ctx context.Context, client database.QueryExecutor, builder *database.StatementBuilder) ([]*domain.ProjectGrant, error) { + rows, err := client.Query(ctx, builder.String(), builder.Args()...) + if err != nil { + return nil, err + } + + var projectGrants []rawProjectGrant + if err = rows.(database.CollectableRows).Collect(&projectGrants); err != nil { + return nil, err + } + + result := make([]*domain.ProjectGrant, len(projectGrants)) + for i := range projectGrants { + result[i] = projectGrants[i].toDomain() + } + return result, nil +} diff --git a/internal/database/type.go b/internal/database/type.go index bd07e0bfd..968bdc185 100644 --- a/internal/database/type.go +++ b/internal/database/type.go @@ -15,6 +15,11 @@ type TextArray[T ~string] pgtype.FlatArray[T] // Scan implements the [database/sql.Scanner] interface. func (s *TextArray[T]) Scan(src any) error { + if src == nil { + *s = nil + return nil + } + var typedArray []string err := pgtype.NewMap().SQLScanner(&typedArray).Scan(src) if err != nil { @@ -29,6 +34,46 @@ func (s *TextArray[T]) Scan(src any) error { return nil } +func (s TextArray[T]) Dimensions() []pgtype.ArrayDimension { + if s == nil { + return nil + } + + return []pgtype.ArrayDimension{{Length: int32(len(s)), LowerBound: 1}} +} + +func (s TextArray[T]) Index(i int) any { + return s[i] +} + +func (s TextArray[T]) IndexType() any { + var el T + return el +} + +func (s *TextArray[T]) SetDimensions(dimensions []pgtype.ArrayDimension) error { + if dimensions == nil { + *s = nil + return nil + } + + elementCount := 1 + for _, dimension := range dimensions { + elementCount *= int(dimension.Length) + } + + *s = make(TextArray[T], elementCount) + return nil +} + +func (s TextArray[T]) ScanIndex(i int) any { + return &s[i] +} + +func (s TextArray[T]) ScanIndexType() any { + return new(T) +} + // Value implements the [database/sql/driver.Valuer] interface. func (s TextArray[T]) Value() (driver.Value, error) { if len(s) == 0 { diff --git a/internal/query/projection/administrator_relational.go b/internal/query/projection/administrator_relational.go new file mode 100644 index 000000000..9a45e6ffe --- /dev/null +++ b/internal/query/projection/administrator_relational.go @@ -0,0 +1,226 @@ +package projection + +import ( + "context" + "database/sql" + + "github.com/zitadel/zitadel/backend/v3/domain" + "github.com/zitadel/zitadel/backend/v3/storage/database" + v3_sql "github.com/zitadel/zitadel/backend/v3/storage/database/dialect/sql" + "github.com/zitadel/zitadel/backend/v3/storage/database/repository" + "github.com/zitadel/zitadel/internal/eventstore" + "github.com/zitadel/zitadel/internal/eventstore/handler/v2" + "github.com/zitadel/zitadel/internal/repository/instance" + "github.com/zitadel/zitadel/internal/repository/org" + "github.com/zitadel/zitadel/internal/repository/project" + "github.com/zitadel/zitadel/internal/zerrors" +) + +func (p *relationalTablesProjection) reduceInstanceAdminAdded(event eventstore.Event) (*handler.Statement, error) { + e, err := assertEvent[*instance.MemberAddedEvent](event) + if err != nil { + return nil, err + } + return p.createAdministratorStatement(e, &domain.Administrator{ + InstanceID: e.Aggregate().InstanceID, + UserID: e.UserID, + Scope: domain.AdministratorScopeInstance, + Roles: e.Roles, + CreatedAt: e.CreationDate(), + UpdatedAt: e.CreationDate(), + }) +} + +func (p *relationalTablesProjection) reduceInstanceAdminChanged(event eventstore.Event) (*handler.Statement, error) { + e, err := assertEvent[*instance.MemberChangedEvent](event) + if err != nil { + return nil, err + } + return p.updateAdministratorStatement(e, + repository.AdministratorRepository().InstanceAdministratorCondition(e.Aggregate().InstanceID, e.UserID), + repository.AdministratorRepository().SetUpdatedAt(e.CreationDate()), + repository.AdministratorRepository().SetRoles(e.Roles), + ) +} + +func (p *relationalTablesProjection) reduceInstanceAdminRemoved(event eventstore.Event) (*handler.Statement, error) { + switch e := event.(type) { + case *instance.MemberRemovedEvent: + return p.removeAdministratorStatement(e, + repository.AdministratorRepository().InstanceAdministratorCondition(e.Aggregate().InstanceID, e.UserID), + ) + case *instance.MemberCascadeRemovedEvent: + return p.removeAdministratorStatement(e, + repository.AdministratorRepository().InstanceAdministratorCondition(e.Aggregate().InstanceID, e.UserID), + ) + default: + return nil, zerrors.ThrowInvalidArgumentf(nil, "HANDL-v3ADM03", "reduce.wrong.event.type %v", []eventstore.EventType{instance.MemberRemovedEventType, instance.MemberCascadeRemovedEventType}) + } +} + +func (p *relationalTablesProjection) reduceOrganizationAdminAdded(event eventstore.Event) (*handler.Statement, error) { + e, err := assertEvent[*org.MemberAddedEvent](event) + if err != nil { + return nil, err + } + return p.createAdministratorStatement(e, &domain.Administrator{ + InstanceID: e.Aggregate().InstanceID, + UserID: e.UserID, + Scope: domain.AdministratorScopeOrganization, + OrganizationID: stringPtr(e.Aggregate().ID), + Roles: e.Roles, + CreatedAt: e.CreationDate(), + UpdatedAt: e.CreationDate(), + }) +} + +func (p *relationalTablesProjection) reduceOrganizationAdminChanged(event eventstore.Event) (*handler.Statement, error) { + e, err := assertEvent[*org.MemberChangedEvent](event) + if err != nil { + return nil, err + } + return p.updateAdministratorStatement(e, + repository.AdministratorRepository().OrganizationAdministratorCondition(e.Aggregate().InstanceID, e.Aggregate().ID, e.UserID), + repository.AdministratorRepository().SetUpdatedAt(e.CreationDate()), + repository.AdministratorRepository().SetRoles(e.Roles), + ) +} + +func (p *relationalTablesProjection) reduceOrganizationAdminRemoved(event eventstore.Event) (*handler.Statement, error) { + switch e := event.(type) { + case *org.MemberRemovedEvent: + return p.removeAdministratorStatement(e, + repository.AdministratorRepository().OrganizationAdministratorCondition(e.Aggregate().InstanceID, e.Aggregate().ID, e.UserID), + ) + case *org.MemberCascadeRemovedEvent: + return p.removeAdministratorStatement(e, + repository.AdministratorRepository().OrganizationAdministratorCondition(e.Aggregate().InstanceID, e.Aggregate().ID, e.UserID), + ) + default: + return nil, zerrors.ThrowInvalidArgumentf(nil, "HANDL-v3ADM06", "reduce.wrong.event.type %v", []eventstore.EventType{org.MemberRemovedEventType, org.MemberCascadeRemovedEventType}) + } +} + +func (p *relationalTablesProjection) reduceProjectAdminAdded(event eventstore.Event) (*handler.Statement, error) { + e, err := assertEvent[*project.MemberAddedEvent](event) + if err != nil { + return nil, err + } + return p.createAdministratorStatement(e, &domain.Administrator{ + InstanceID: e.Aggregate().InstanceID, + UserID: e.UserID, + Scope: domain.AdministratorScopeProject, + ProjectID: stringPtr(e.Aggregate().ID), + Roles: e.Roles, + CreatedAt: e.CreationDate(), + UpdatedAt: e.CreationDate(), + }) +} + +func (p *relationalTablesProjection) reduceProjectAdminChanged(event eventstore.Event) (*handler.Statement, error) { + e, err := assertEvent[*project.MemberChangedEvent](event) + if err != nil { + return nil, err + } + return p.updateAdministratorStatement(e, + repository.AdministratorRepository().ProjectAdministratorCondition(e.Aggregate().InstanceID, e.Aggregate().ID, e.UserID), + repository.AdministratorRepository().SetUpdatedAt(e.CreationDate()), + repository.AdministratorRepository().SetRoles(e.Roles), + ) +} + +func (p *relationalTablesProjection) reduceProjectAdminRemoved(event eventstore.Event) (*handler.Statement, error) { + switch e := event.(type) { + case *project.MemberRemovedEvent: + return p.removeAdministratorStatement(e, + repository.AdministratorRepository().ProjectAdministratorCondition(e.Aggregate().InstanceID, e.Aggregate().ID, e.UserID), + ) + case *project.MemberCascadeRemovedEvent: + return p.removeAdministratorStatement(e, + repository.AdministratorRepository().ProjectAdministratorCondition(e.Aggregate().InstanceID, e.Aggregate().ID, e.UserID), + ) + default: + return nil, zerrors.ThrowInvalidArgumentf(nil, "HANDL-v3ADM09", "reduce.wrong.event.type %v", []eventstore.EventType{project.MemberRemovedEventType, project.MemberCascadeRemovedEventType}) + } +} + +func (p *relationalTablesProjection) reduceProjectGrantAdminAdded(event eventstore.Event) (*handler.Statement, error) { + e, err := assertEvent[*project.GrantMemberAddedEvent](event) + if err != nil { + return nil, err + } + return p.createAdministratorStatement(e, &domain.Administrator{ + InstanceID: e.Aggregate().InstanceID, + UserID: e.UserID, + Scope: domain.AdministratorScopeProjectGrant, + ProjectGrantID: stringPtr(e.GrantID), + Roles: e.Roles, + CreatedAt: e.CreationDate(), + UpdatedAt: e.CreationDate(), + }) +} + +func (p *relationalTablesProjection) reduceProjectGrantAdminChanged(event eventstore.Event) (*handler.Statement, error) { + e, err := assertEvent[*project.GrantMemberChangedEvent](event) + if err != nil { + return nil, err + } + return p.updateAdministratorStatement(e, + repository.AdministratorRepository().ProjectGrantAdministratorCondition(e.Aggregate().InstanceID, e.GrantID, e.UserID), + repository.AdministratorRepository().SetUpdatedAt(e.CreationDate()), + repository.AdministratorRepository().SetRoles(e.Roles), + ) +} + +func (p *relationalTablesProjection) reduceProjectGrantAdminRemoved(event eventstore.Event) (*handler.Statement, error) { + switch e := event.(type) { + case *project.GrantMemberRemovedEvent: + return p.removeAdministratorStatement(e, + repository.AdministratorRepository().ProjectGrantAdministratorCondition(e.Aggregate().InstanceID, e.GrantID, e.UserID), + ) + case *project.GrantMemberCascadeRemovedEvent: + return p.removeAdministratorStatement(e, + repository.AdministratorRepository().ProjectGrantAdministratorCondition(e.Aggregate().InstanceID, e.GrantID, e.UserID), + ) + default: + return nil, zerrors.ThrowInvalidArgumentf(nil, "HANDL-v3ADM12", "reduce.wrong.event.type %v", []eventstore.EventType{project.GrantMemberRemovedType, project.GrantMemberCascadeRemovedType}) + } +} + +func (p *relationalTablesProjection) createAdministratorStatement(event eventstore.Event, administrator *domain.Administrator) (*handler.Statement, error) { + 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-v3ADM13", "reduce.wrong.db.pool %T", ex) + } + return repository.AdministratorRepository().Create(ctx, v3_sql.SQLTx(tx), administrator) + }), nil +} + +func (p *relationalTablesProjection) updateAdministratorStatement(event eventstore.Event, condition database.Condition, changes ...database.Change) (*handler.Statement, error) { + 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-v3ADM14", "reduce.wrong.db.pool %T", ex) + } + repo := repository.AdministratorRepository() + _, err := repo.Update(ctx, v3_sql.SQLTx(tx), condition, changes...) + return err + }), nil +} + +func (p *relationalTablesProjection) removeAdministratorStatement(event eventstore.Event, condition database.Condition) (*handler.Statement, error) { + 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-v3ADM15", "reduce.wrong.db.pool %T", ex) + } + repo := repository.AdministratorRepository() + _, err := repo.Delete(ctx, v3_sql.SQLTx(tx), condition) + return err + }), nil +} + +func stringPtr(value string) *string { + return &value +} diff --git a/internal/query/projection/administrator_relational_test.go b/internal/query/projection/administrator_relational_test.go new file mode 100644 index 000000000..692f42b4e --- /dev/null +++ b/internal/query/projection/administrator_relational_test.go @@ -0,0 +1,369 @@ +package projection + +import ( + "fmt" + "testing" + "time" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + + repoDomain "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/repository/instance" + "github.com/zitadel/zitadel/internal/repository/org" + "github.com/zitadel/zitadel/internal/repository/project" +) + +func TestAdministratorRelationalReducers(t *testing.T) { + handler := new(relationalTablesProjection) + rawTx, tx := getTransactions(t) + t.Cleanup(func() { + require.NoError(t, rawTx.Rollback()) + }) + + instanceID, _, orgID, projectID, grantID := seedAdministratorRelationalState(t, tx) + adminRepo := repository.AdministratorRepository() + userRepo := repository.UserRepository() + + var userSeq int + createUser := func(t *testing.T) string { + t.Helper() + userSeq++ + userID := fmt.Sprintf("admin-test-user-%d", userSeq) + err := userRepo.Create(t.Context(), tx, &repoDomain.User{ + InstanceID: instanceID, + OrganizationID: orgID, + ID: userID, + Username: userID, + State: repoDomain.UserStateActive, + Machine: &repoDomain.MachineUser{ + Name: "machine", + }, + CreatedAt: time.Now(), + UpdatedAt: time.Now(), + }) + require.NoError(t, err) + return userID + } + + t.Run("'instance member added' reducer should create instance administrator", func(t *testing.T) { + userID := createUser(t) + + added := instance.NewMemberAddedEvent(t.Context(), &instance.NewAggregate(instanceID).Aggregate, userID, "IAM_OWNER") + require.True(t, callReduce(t, rawTx, handler, added)) + + admin, err := adminRepo.Get(t.Context(), tx, database.WithCondition( + adminRepo.InstanceAdministratorCondition(instanceID, userID), + )) + require.NoError(t, err) + assert.Equal(t, []string{"IAM_OWNER"}, admin.Roles) + }) + + t.Run("'instance member changed' reducer should update instance administrator roles", func(t *testing.T) { + userID := createUser(t) + err := adminRepo.Create(t.Context(), tx, &repoDomain.Administrator{ + InstanceID: instanceID, + UserID: userID, + Scope: repoDomain.AdministratorScopeInstance, + Roles: []string{"IAM_OWNER"}, + }) + require.NoError(t, err) + + changed := instance.NewMemberChangedEvent(t.Context(), &instance.NewAggregate(instanceID).Aggregate, userID, "IAM_OWNER_VIEWER") + require.True(t, callReduce(t, rawTx, handler, changed)) + + admin, err := adminRepo.Get(t.Context(), tx, database.WithCondition( + adminRepo.InstanceAdministratorCondition(instanceID, userID), + )) + require.NoError(t, err) + assert.Equal(t, []string{"IAM_OWNER_VIEWER"}, admin.Roles) + }) + + t.Run("'instance member removed' reducer should remove instance administrator", func(t *testing.T) { + userID := createUser(t) + err := adminRepo.Create(t.Context(), tx, &repoDomain.Administrator{ + InstanceID: instanceID, + UserID: userID, + Scope: repoDomain.AdministratorScopeInstance, + Roles: []string{"IAM_OWNER"}, + }) + require.NoError(t, err) + + removed := instance.NewMemberRemovedEvent(t.Context(), &instance.NewAggregate(instanceID).Aggregate, userID) + require.True(t, callReduce(t, rawTx, handler, removed)) + + _, err = adminRepo.Get(t.Context(), tx, database.WithCondition( + adminRepo.InstanceAdministratorCondition(instanceID, userID), + )) + require.ErrorIs(t, err, database.NewNoRowFoundError(nil)) + }) + + t.Run("'org member added' reducer should create organization administrator", func(t *testing.T) { + userID := createUser(t) + + orgAggregate := org.NewAggregate(orgID) + orgAggregate.InstanceID = instanceID + + added := org.NewMemberAddedEvent(t.Context(), &orgAggregate.Aggregate, userID, "ORG_OWNER") + require.True(t, callReduce(t, rawTx, handler, added)) + + admin, err := adminRepo.Get(t.Context(), tx, database.WithCondition( + adminRepo.OrganizationAdministratorCondition(instanceID, orgID, userID), + )) + require.NoError(t, err) + assert.Equal(t, []string{"ORG_OWNER"}, admin.Roles) + }) + + t.Run("'org member changed' reducer should update organization administrator roles", func(t *testing.T) { + userID := createUser(t) + err := adminRepo.Create(t.Context(), tx, &repoDomain.Administrator{ + InstanceID: instanceID, + UserID: userID, + Scope: repoDomain.AdministratorScopeOrganization, + OrganizationID: &orgID, + Roles: []string{"ORG_OWNER"}, + }) + require.NoError(t, err) + + orgAggregate := org.NewAggregate(orgID) + orgAggregate.InstanceID = instanceID + + changed := org.NewMemberChangedEvent(t.Context(), &orgAggregate.Aggregate, userID, "ORG_OWNER_VIEWER") + require.True(t, callReduce(t, rawTx, handler, changed)) + + admin, err := adminRepo.Get(t.Context(), tx, database.WithCondition( + adminRepo.OrganizationAdministratorCondition(instanceID, orgID, userID), + )) + require.NoError(t, err) + assert.Equal(t, []string{"ORG_OWNER_VIEWER"}, admin.Roles) + }) + + t.Run("'org member removed' reducer should remove organization administrator", func(t *testing.T) { + userID := createUser(t) + err := adminRepo.Create(t.Context(), tx, &repoDomain.Administrator{ + InstanceID: instanceID, + UserID: userID, + Scope: repoDomain.AdministratorScopeOrganization, + OrganizationID: &orgID, + Roles: []string{"ORG_OWNER"}, + }) + require.NoError(t, err) + + orgAggregate := org.NewAggregate(orgID) + orgAggregate.InstanceID = instanceID + + removed := org.NewMemberRemovedEvent(t.Context(), &orgAggregate.Aggregate, userID) + require.True(t, callReduce(t, rawTx, handler, removed)) + + _, err = adminRepo.Get(t.Context(), tx, database.WithCondition( + adminRepo.OrganizationAdministratorCondition(instanceID, orgID, userID), + )) + require.ErrorIs(t, err, database.NewNoRowFoundError(nil)) + }) + + t.Run("'project member added' reducer should create project administrator", func(t *testing.T) { + userID := createUser(t) + + projectAggregate := project.NewAggregate(projectID, orgID) + projectAggregate.InstanceID = instanceID + + added := project.NewProjectMemberAddedEvent(t.Context(), &projectAggregate.Aggregate, userID, "PROJECT_OWNER") + require.True(t, callReduce(t, rawTx, handler, added)) + + admin, err := adminRepo.Get(t.Context(), tx, database.WithCondition( + adminRepo.ProjectAdministratorCondition(instanceID, projectID, userID), + )) + require.NoError(t, err) + assert.Equal(t, []string{"PROJECT_OWNER"}, admin.Roles) + }) + + t.Run("'project member changed' reducer should update project administrator roles", func(t *testing.T) { + userID := createUser(t) + err := adminRepo.Create(t.Context(), tx, &repoDomain.Administrator{ + InstanceID: instanceID, + UserID: userID, + Scope: repoDomain.AdministratorScopeProject, + ProjectID: &projectID, + Roles: []string{"PROJECT_OWNER"}, + }) + require.NoError(t, err) + + projectAggregate := project.NewAggregate(projectID, orgID) + projectAggregate.InstanceID = instanceID + + changed := project.NewProjectMemberChangedEvent(t.Context(), &projectAggregate.Aggregate, userID, "PROJECT_OWNER_VIEWER") + require.True(t, callReduce(t, rawTx, handler, changed)) + + admin, err := adminRepo.Get(t.Context(), tx, database.WithCondition( + adminRepo.ProjectAdministratorCondition(instanceID, projectID, userID), + )) + require.NoError(t, err) + assert.Equal(t, []string{"PROJECT_OWNER_VIEWER"}, admin.Roles) + }) + + t.Run("'project member removed' reducer should remove project administrator", func(t *testing.T) { + userID := createUser(t) + err := adminRepo.Create(t.Context(), tx, &repoDomain.Administrator{ + InstanceID: instanceID, + UserID: userID, + Scope: repoDomain.AdministratorScopeProject, + ProjectID: &projectID, + Roles: []string{"PROJECT_OWNER"}, + }) + require.NoError(t, err) + + projectAggregate := project.NewAggregate(projectID, orgID) + projectAggregate.InstanceID = instanceID + + removed := project.NewProjectMemberRemovedEvent(t.Context(), &projectAggregate.Aggregate, userID) + require.True(t, callReduce(t, rawTx, handler, removed)) + + _, err = adminRepo.Get(t.Context(), tx, database.WithCondition( + adminRepo.ProjectAdministratorCondition(instanceID, projectID, userID), + )) + require.ErrorIs(t, err, database.NewNoRowFoundError(nil)) + }) + + t.Run("'project grant member added' reducer should create project grant administrator", func(t *testing.T) { + userID := createUser(t) + + projectAggregate := project.NewAggregate(projectID, orgID) + projectAggregate.InstanceID = instanceID + + added := project.NewProjectGrantMemberAddedEvent(t.Context(), &projectAggregate.Aggregate, userID, grantID, "PROJECT_OWNER") + require.True(t, callReduce(t, rawTx, handler, added)) + + admin, err := adminRepo.Get(t.Context(), tx, database.WithCondition( + adminRepo.ProjectGrantAdministratorCondition(instanceID, grantID, userID), + )) + require.NoError(t, err) + assert.Equal(t, []string{"PROJECT_OWNER"}, admin.Roles) + }) + + t.Run("'project grant member changed' reducer should update project grant administrator roles", func(t *testing.T) { + userID := createUser(t) + err := adminRepo.Create(t.Context(), tx, &repoDomain.Administrator{ + InstanceID: instanceID, + UserID: userID, + Scope: repoDomain.AdministratorScopeProjectGrant, + ProjectGrantID: &grantID, + Roles: []string{"PROJECT_OWNER"}, + }) + require.NoError(t, err) + + projectAggregate := project.NewAggregate(projectID, orgID) + projectAggregate.InstanceID = instanceID + + changed := project.NewProjectGrantMemberChangedEvent(t.Context(), &projectAggregate.Aggregate, userID, grantID, "PROJECT_OWNER_VIEWER") + require.True(t, callReduce(t, rawTx, handler, changed)) + + admin, err := adminRepo.Get(t.Context(), tx, database.WithCondition( + adminRepo.ProjectGrantAdministratorCondition(instanceID, grantID, userID), + )) + require.NoError(t, err) + assert.Equal(t, []string{"PROJECT_OWNER_VIEWER"}, admin.Roles) + }) + + t.Run("'project grant member cascade removed' reducer should remove project grant administrator", func(t *testing.T) { + userID := createUser(t) + err := adminRepo.Create(t.Context(), tx, &repoDomain.Administrator{ + InstanceID: instanceID, + UserID: userID, + Scope: repoDomain.AdministratorScopeProjectGrant, + ProjectGrantID: &grantID, + Roles: []string{"PROJECT_OWNER"}, + }) + require.NoError(t, err) + + projectAggregate := project.NewAggregate(projectID, orgID) + projectAggregate.InstanceID = instanceID + + removed := project.NewProjectGrantMemberCascadeRemovedEvent(t.Context(), &projectAggregate.Aggregate, userID, grantID) + require.True(t, callReduce(t, rawTx, handler, removed)) + + _, err = adminRepo.Get(t.Context(), tx, database.WithCondition( + adminRepo.ProjectGrantAdministratorCondition(instanceID, grantID, userID), + )) + require.ErrorIs(t, err, database.NewNoRowFoundError(nil)) + }) +} + +func seedAdministratorRelationalState(t *testing.T, tx database.QueryExecutor) (instanceID, userID, orgID, projectID, projectGrantID string) { + t.Helper() + + instanceRepo := repository.InstanceRepository() + orgRepo := repository.OrganizationRepository() + projectRepo := repository.ProjectRepository() + projectGrantRepo := repository.ProjectGrantRepository() + userRepo := repository.UserRepository() + + now := time.Now().UnixNano() + instanceID = fmt.Sprintf("instance-%d", now) + err := instanceRepo.Create(t.Context(), tx, &repoDomain.Instance{ + ID: instanceID, + Name: "instance", + DefaultOrgID: "default-org", + IAMProjectID: "iam-project", + ConsoleClientID: "console-client", + ConsoleAppID: "console-app", + DefaultLanguage: "en", + CreatedAt: time.Now(), + UpdatedAt: time.Now(), + }) + require.NoError(t, err) + + orgID = fmt.Sprintf("org-%d", now+1) + err = orgRepo.Create(t.Context(), tx, &repoDomain.Organization{ + InstanceID: instanceID, + ID: orgID, + Name: "org", + State: repoDomain.OrgStateActive, + CreatedAt: time.Now(), + UpdatedAt: time.Now(), + }) + require.NoError(t, err) + + projectID = fmt.Sprintf("project-%d", now+2) + err = projectRepo.Create(t.Context(), tx, &repoDomain.Project{ + InstanceID: instanceID, + OrganizationID: orgID, + ID: projectID, + Name: "project", + State: repoDomain.ProjectStateActive, + CreatedAt: time.Now(), + UpdatedAt: time.Now(), + }) + require.NoError(t, err) + + projectGrantID = fmt.Sprintf("grant-%d", now+3) + err = projectGrantRepo.Create(t.Context(), tx, &repoDomain.ProjectGrant{ + InstanceID: instanceID, + ID: projectGrantID, + ProjectID: projectID, + GrantingOrganizationID: orgID, + GrantedOrganizationID: orgID, + State: repoDomain.ProjectGrantStateActive, + CreatedAt: time.Now(), + UpdatedAt: time.Now(), + }) + require.NoError(t, err) + + userID = fmt.Sprintf("user-%d", now+4) + err = userRepo.Create(t.Context(), tx, &repoDomain.User{ + InstanceID: instanceID, + OrganizationID: orgID, + ID: userID, + Username: userID, + State: repoDomain.UserStateActive, + Machine: &repoDomain.MachineUser{ + Name: "machine", + }, + CreatedAt: time.Now(), + UpdatedAt: time.Now(), + }) + require.NoError(t, err) + + return instanceID, userID, orgID, projectID, projectGrantID +} diff --git a/internal/query/projection/event_testing_relational_test.go b/internal/query/projection/event_testing_relational_test.go index 30ea53f6f..18bcc26d2 100644 --- a/internal/query/projection/event_testing_relational_test.go +++ b/internal/query/projection/event_testing_relational_test.go @@ -85,7 +85,7 @@ func getTransactions(t *testing.T) (rawTx *sql.Tx, v3SQLTx *v3_sql.Transaction) // 4. the reducer function for the input event executes without errors // // Returns false if any of the above checks fails -func callReduce(t *testing.T, ctx context.Context, tx *sql.Tx, projection handler.Projection, event eventstore.Event) bool { +func callReduce(t *testing.T, tx *sql.Tx, projection handler.Projection, event eventstore.Event) bool { reducers := projection.Reducers() aggregateReducerIdx := slices.IndexFunc(reducers, func(r handler.AggregateReducer) bool { return r.Aggregate == event.Aggregate().Type @@ -111,6 +111,6 @@ func callReduce(t *testing.T, ctx context.Context, tx *sql.Tx, projection handle return false } - err = stmt.Execute(ctx, tx, "") + err = stmt.Execute(t.Context(), tx, "") return assert.NoError(t, err) } diff --git a/internal/query/projection/instance_domain_relational_test.go b/internal/query/projection/instance_domain_relational_test.go index 73f14aae0..6faf3418a 100644 --- a/internal/query/projection/instance_domain_relational_test.go +++ b/internal/query/projection/instance_domain_relational_test.go @@ -44,7 +44,7 @@ func TestInstanceDomainReducers(t *testing.T) { domainAddedEvt := instance.NewDomainAddedEvent(ctx, &instance.NewAggregate("123").Aggregate, domainName, false) // Test - res := callReduce(t, ctx, rawTx, handler, domainAddedEvt) + res := callReduce(t, rawTx, handler, domainAddedEvt) require.True(t, res) // Verify @@ -85,7 +85,7 @@ func TestInstanceDomainReducers(t *testing.T) { domainPrimarySetEvt := instance.NewDomainPrimarySetEvent(ctx, &instance.NewAggregate("123").Aggregate, domainName) // Test - res := callReduce(t, ctx, rawTx, handler, domainPrimarySetEvt) + res := callReduce(t, rawTx, handler, domainPrimarySetEvt) require.True(t, res) // Verify @@ -127,7 +127,7 @@ func TestInstanceDomainReducers(t *testing.T) { domainRemovedEvt := instance.NewDomainRemovedEvent(ctx, &instance.NewAggregate("123").Aggregate, domainName) // Test - res := callReduce(t, ctx, rawTx, handler, domainRemovedEvt) + res := callReduce(t, rawTx, handler, domainRemovedEvt) require.True(t, res) // Verify @@ -150,7 +150,7 @@ func TestInstanceDomainReducers(t *testing.T) { trustedDomainAddedEvt := instance.NewTrustedDomainAddedEvent(ctx, &instance.NewAggregate("123").Aggregate, domainName) // Test - res := callReduce(t, ctx, rawTx, handler, trustedDomainAddedEvt) + res := callReduce(t, rawTx, handler, trustedDomainAddedEvt) require.True(t, res) // Verify @@ -185,7 +185,7 @@ func TestInstanceDomainReducers(t *testing.T) { trustedDomainRemovedEvt := instance.NewTrustedDomainRemovedEvent(ctx, &instance.NewAggregate("123").Aggregate, domainName) // Test - res := callReduce(t, ctx, rawTx, handler, trustedDomainRemovedEvt) + res := callReduce(t, rawTx, handler, trustedDomainRemovedEvt) require.True(t, res) // Verify diff --git a/internal/query/projection/relational_table.go b/internal/query/projection/relational_table.go index 6d51e84bd..5d4f03748 100644 --- a/internal/query/projection/relational_table.go +++ b/internal/query/projection/relational_table.go @@ -377,6 +377,24 @@ func (p *relationalTablesProjection) Reducers() []handler.AggregateReducer { Event: instance.IDPRemovedEventType, Reduce: p.reduceIDPRemoved, }, + + // Administrators + { + Event: instance.MemberAddedEventType, + Reduce: p.reduceInstanceAdminAdded, + }, + { + Event: instance.MemberChangedEventType, + Reduce: p.reduceInstanceAdminChanged, + }, + { + Event: instance.MemberRemovedEventType, + Reduce: p.reduceInstanceAdminRemoved, + }, + { + Event: instance.MemberCascadeRemovedEventType, + Reduce: p.reduceInstanceAdminRemoved, + }, }, }, { @@ -750,6 +768,24 @@ func (p *relationalTablesProjection) Reducers() []handler.AggregateReducer { Event: org.IDPRemovedEventType, Reduce: p.reduceIDPRemoved, }, + + // Administrators + { + Event: org.MemberAddedEventType, + Reduce: p.reduceOrganizationAdminAdded, + }, + { + Event: org.MemberChangedEventType, + Reduce: p.reduceOrganizationAdminChanged, + }, + { + Event: org.MemberRemovedEventType, + Reduce: p.reduceOrganizationAdminRemoved, + }, + { + Event: org.MemberCascadeRemovedEventType, + Reduce: p.reduceOrganizationAdminRemoved, + }, }, }, { @@ -828,6 +864,40 @@ func (p *relationalTablesProjection) Reducers() []handler.AggregateReducer { Event: project.RoleRemovedType, Reduce: p.reduceProjectRoleRemoved, }, + + // Administrators + { + Event: project.MemberAddedEventType, + Reduce: p.reduceProjectAdminAdded, + }, + { + Event: project.MemberChangedEventType, + Reduce: p.reduceProjectAdminChanged, + }, + { + Event: project.MemberRemovedEventType, + Reduce: p.reduceProjectAdminRemoved, + }, + { + Event: project.MemberCascadeRemovedEventType, + Reduce: p.reduceProjectAdminRemoved, + }, + { + Event: project.GrantMemberAddedType, + Reduce: p.reduceProjectGrantAdminAdded, + }, + { + Event: project.GrantMemberChangedType, + Reduce: p.reduceProjectGrantAdminChanged, + }, + { + Event: project.GrantMemberRemovedType, + Reduce: p.reduceProjectGrantAdminRemoved, + }, + { + Event: project.GrantMemberCascadeRemovedType, + Reduce: p.reduceProjectGrantAdminRemoved, + }, }, }, { diff --git a/internal/query/projection/user_relational_test.go b/internal/query/projection/user_relational_test.go index fe0c4b36c..1328e852d 100644 --- a/internal/query/projection/user_relational_test.go +++ b/internal/query/projection/user_relational_test.go @@ -1,7 +1,6 @@ package projection import ( - "context" "testing" "time" @@ -23,13 +22,11 @@ func TestUserRelationalProjection_Reducers(t *testing.T) { require.NoError(t, rawTx.Rollback()) }) - ctx := t.Context() - // create instance instanceRepo := repository.InstanceRepository() instanceID := gofakeit.UUID() orgID := gofakeit.UUID() - err := instanceRepo.Create(ctx, tx, &domain.Instance{ + err := instanceRepo.Create(t.Context(), tx, &domain.Instance{ ID: instanceID, Name: "test-instance", DefaultOrgID: orgID, @@ -38,7 +35,7 @@ func TestUserRelationalProjection_Reducers(t *testing.T) { // create org orgRepo := repository.OrganizationRepository() - err = orgRepo.Create(ctx, tx, &domain.Organization{ + err = orgRepo.Create(t.Context(), tx, &domain.Organization{ InstanceID: instanceID, ID: orgID, Name: "test-org", @@ -54,22 +51,22 @@ func TestUserRelationalProjection_Reducers(t *testing.T) { t.Run("reduce user.human.mfa.recoverycode.added event", func(t *testing.T) { // create user existingUserID := gofakeit.UUID() - existingUserAgg := createUser(t, ctx, tx, userRepo, instanceID, orgID, existingUserID) + existingUserAgg := createUser(t, tx, userRepo, instanceID, orgID, existingUserID) // set `user.human.mfa.recoverycode.added` event recoveryCodes := []string{"code1", "code2", "code3"} recoveryCodesAddedEvent := user.NewHumanRecoveryCodesAddedEvent( - ctx, + t.Context(), &existingUserAgg.Aggregate, recoveryCodes, nil) // reduce the event - eventReduced := callReduce(t, ctx, rawTx, handler, recoveryCodesAddedEvent) + eventReduced := callReduce(t, rawTx, handler, recoveryCodesAddedEvent) require.True(t, eventReduced) // assert that the recovery codes are stored - gotUser, err := userRepo.Get(ctx, tx, database.WithCondition( + gotUser, err := userRepo.Get(t.Context(), tx, database.WithCondition( database.And( userRepo.IDCondition(existingUserID), userRepo.InstanceIDCondition(instanceID), @@ -84,26 +81,26 @@ func TestUserRelationalProjection_Reducers(t *testing.T) { t.Run("reduce user.human.mfa.recoverycode.removed event", func(t *testing.T) { // create user existingUserID := gofakeit.UUID() - existingUserAgg := createUser(t, ctx, tx, userRepo, instanceID, orgID, existingUserID) + existingUserAgg := createUser(t, tx, userRepo, instanceID, orgID, existingUserID) // add recovery codes to the user - _, err = userRepo.Update(ctx, tx, + _, err = userRepo.Update(t.Context(), tx, userRepo.PrimaryKeyCondition(instanceID, existingUserID), userRepo.Human().AddRecoveryCodes([]string{"code1", "code2", "code3"})) require.NoError(t, err) // set `user.human.mfa.recoverycode.removed` event recoveryCodesRemovedEvent := user.NewHumanRecoveryCodeRemovedEvent( - ctx, + t.Context(), &existingUserAgg.Aggregate, nil) // reduce the event - eventReduced := callReduce(t, ctx, rawTx, handler, recoveryCodesRemovedEvent) + eventReduced := callReduce(t, rawTx, handler, recoveryCodesRemovedEvent) require.True(t, eventReduced) // assert that the recovery codes are removed - gotUser, err := userRepo.Get(ctx, tx, database.WithCondition( + gotUser, err := userRepo.Get(t.Context(), tx, database.WithCondition( database.And( userRepo.IDCondition(existingUserID), userRepo.InstanceIDCondition(instanceID), @@ -118,27 +115,27 @@ func TestUserRelationalProjection_Reducers(t *testing.T) { t.Run("reduce user.human.mfa.recoverycode.check.succeeded event", func(t *testing.T) { // create user existingUserID := gofakeit.UUID() - existingUserAgg := createUser(t, ctx, tx, userRepo, instanceID, orgID, existingUserID) + existingUserAgg := createUser(t, tx, userRepo, instanceID, orgID, existingUserID) // add recovery codes to the user - _, err = userRepo.Update(ctx, tx, + _, err = userRepo.Update(t.Context(), tx, userRepo.PrimaryKeyCondition(instanceID, existingUserID), userRepo.Human().AddRecoveryCodes([]string{"code1", "code2", "code3"})) require.NoError(t, err) // set `user.human.mfa.recoverycode.check.succeeded` event recoveryCodeCheckSucceededEvent := user.NewHumanRecoveryCodeCheckSucceededEvent( - ctx, + t.Context(), &existingUserAgg.Aggregate, "code1", nil) // reduce the event - eventReduced := callReduce(t, ctx, rawTx, handler, recoveryCodeCheckSucceededEvent) + eventReduced := callReduce(t, rawTx, handler, recoveryCodeCheckSucceededEvent) require.True(t, eventReduced) // assert that the recovery code is removed and the last_successfully_checked_at timestamp is set - gotUser, err := userRepo.Get(ctx, tx, database.WithCondition( + gotUser, err := userRepo.Get(t.Context(), tx, database.WithCondition( database.And( userRepo.IDCondition(existingUserID), userRepo.InstanceIDCondition(instanceID), @@ -153,27 +150,27 @@ func TestUserRelationalProjection_Reducers(t *testing.T) { t.Run("reduce user.human.mfa.recoverycode.check.failed event", func(t *testing.T) { // create user existingUserID := gofakeit.UUID() - existingUserAgg := createUser(t, ctx, tx, userRepo, instanceID, orgID, existingUserID) + existingUserAgg := createUser(t, tx, userRepo, instanceID, orgID, existingUserID) // add recovery codes to the user - _, err = userRepo.Update(ctx, tx, + _, err = userRepo.Update(t.Context(), tx, userRepo.PrimaryKeyCondition(instanceID, existingUserID), userRepo.Human().AddRecoveryCodes([]string{"code1", "code2", "code3"})) require.NoError(t, err) // set `user.human.mfa.recoverycode.check.failed` event recoveryCodeCheckFailedEvent := user.NewHumanRecoveryCodeCheckFailedEvent( - ctx, + t.Context(), &existingUserAgg.Aggregate, nil, ) // reduce the event - eventReduced := callReduce(t, ctx, rawTx, handler, recoveryCodeCheckFailedEvent) + eventReduced := callReduce(t, rawTx, handler, recoveryCodeCheckFailedEvent) require.True(t, eventReduced) // assert that the recovery code failed_attempts is incremented - gotUser, err := userRepo.Get(ctx, tx, database.WithCondition( + gotUser, err := userRepo.Get(t.Context(), tx, database.WithCondition( database.And( userRepo.IDCondition(existingUserID), userRepo.InstanceIDCondition(instanceID), @@ -187,7 +184,6 @@ func TestUserRelationalProjection_Reducers(t *testing.T) { } func createUser(t *testing.T, - ctx context.Context, tx *sql.Transaction, userRepo domain.UserRepository, instanceID, @@ -197,7 +193,7 @@ func createUser(t *testing.T, userAgg := user.NewAggregate(userID, orgID) userAgg.InstanceID = instanceID - err := userRepo.Create(ctx, tx, &domain.User{ + err := userRepo.Create(t.Context(), tx, &domain.User{ InstanceID: instanceID, OrganizationID: orgID, ID: userID,