diff --git a/backend/v3/domain/administrator_role.go b/backend/v3/domain/administrator_role.go new file mode 100644 index 000000000..6410dea9c --- /dev/null +++ b/backend/v3/domain/administrator_role.go @@ -0,0 +1,32 @@ +package domain + +import ( + "context" + + "github.com/zitadel/zitadel/backend/v3/storage/database" +) + +type administratorRoleColumns interface { + PrimaryKeyColumns() []database.Column + InstanceIDColumn() database.Column + RoleNameColumn() database.Column + PermissionColumn() database.Column +} + +type administratorRoleConditions interface { + PrimaryKeyCondition(instanceID, roleName, permission string) database.Condition + InstanceIDCondition(instanceID string) database.Condition + RoleNameCondition(op database.TextOperation, name string) database.Condition + PermissionCondition(op database.TextOperation, permission string) database.Condition +} + +//go:generate mockgen -typed -package domainmock -destination ./mock/administrator_role.mock.go . AdministratorRoleRepository +type AdministratorRoleRepository interface { + Repository + + administratorRoleColumns + administratorRoleConditions + + AddPermissions(ctx context.Context, client database.QueryExecutor, instanceID, role string, permissions ...string) (int64, error) + RemovePermissions(ctx context.Context, client database.QueryExecutor, instanceID, role string, permissionsToRemove ...string) (int64, error) +} diff --git a/backend/v3/domain/mock/administrator_role.mock.go b/backend/v3/domain/mock/administrator_role.mock.go new file mode 100644 index 000000000..e0abbb0fa --- /dev/null +++ b/backend/v3/domain/mock/administrator_role.mock.go @@ -0,0 +1,434 @@ +// Code generated by MockGen. DO NOT EDIT. +// Source: github.com/zitadel/zitadel/backend/v3/domain (interfaces: AdministratorRoleRepository) +// +// Generated by this command: +// +// mockgen -typed -package domainmock -destination ./mock/administrator_role.mock.go . AdministratorRoleRepository +// + +// Package domainmock is a generated GoMock package. +package domainmock + +import ( + context "context" + reflect "reflect" + + database "github.com/zitadel/zitadel/backend/v3/storage/database" + gomock "go.uber.org/mock/gomock" +) + +// MockAdministratorRoleRepository is a mock of AdministratorRoleRepository interface. +type MockAdministratorRoleRepository struct { + ctrl *gomock.Controller + recorder *MockAdministratorRoleRepositoryMockRecorder + isgomock struct{} +} + +// MockAdministratorRoleRepositoryMockRecorder is the mock recorder for MockAdministratorRoleRepository. +type MockAdministratorRoleRepositoryMockRecorder struct { + mock *MockAdministratorRoleRepository +} + +// NewMockAdministratorRoleRepository creates a new mock instance. +func NewMockAdministratorRoleRepository(ctrl *gomock.Controller) *MockAdministratorRoleRepository { + mock := &MockAdministratorRoleRepository{ctrl: ctrl} + mock.recorder = &MockAdministratorRoleRepositoryMockRecorder{mock} + return mock +} + +// EXPECT returns an object that allows the caller to indicate expected use. +func (m *MockAdministratorRoleRepository) EXPECT() *MockAdministratorRoleRepositoryMockRecorder { + return m.recorder +} + +// AddPermissions mocks base method. +func (m *MockAdministratorRoleRepository) AddPermissions(ctx context.Context, client database.QueryExecutor, instanceID, role string, permissions ...string) (int64, error) { + m.ctrl.T.Helper() + varargs := []any{ctx, client, instanceID, role} + for _, a := range permissions { + varargs = append(varargs, a) + } + ret := m.ctrl.Call(m, "AddPermissions", varargs...) + ret0, _ := ret[0].(int64) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// AddPermissions indicates an expected call of AddPermissions. +func (mr *MockAdministratorRoleRepositoryMockRecorder) AddPermissions(ctx, client, instanceID, role any, permissions ...any) *MockAdministratorRoleRepositoryAddPermissionsCall { + mr.mock.ctrl.T.Helper() + varargs := append([]any{ctx, client, instanceID, role}, permissions...) + call := mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "AddPermissions", reflect.TypeOf((*MockAdministratorRoleRepository)(nil).AddPermissions), varargs...) + return &MockAdministratorRoleRepositoryAddPermissionsCall{Call: call} +} + +// MockAdministratorRoleRepositoryAddPermissionsCall wrap *gomock.Call +type MockAdministratorRoleRepositoryAddPermissionsCall struct { + *gomock.Call +} + +// Return rewrite *gomock.Call.Return +func (c *MockAdministratorRoleRepositoryAddPermissionsCall) Return(arg0 int64, arg1 error) *MockAdministratorRoleRepositoryAddPermissionsCall { + c.Call = c.Call.Return(arg0, arg1) + return c +} + +// Do rewrite *gomock.Call.Do +func (c *MockAdministratorRoleRepositoryAddPermissionsCall) Do(f func(context.Context, database.QueryExecutor, string, string, ...string) (int64, error)) *MockAdministratorRoleRepositoryAddPermissionsCall { + c.Call = c.Call.Do(f) + return c +} + +// DoAndReturn rewrite *gomock.Call.DoAndReturn +func (c *MockAdministratorRoleRepositoryAddPermissionsCall) DoAndReturn(f func(context.Context, database.QueryExecutor, string, string, ...string) (int64, error)) *MockAdministratorRoleRepositoryAddPermissionsCall { + c.Call = c.Call.DoAndReturn(f) + return c +} + +// InstanceIDColumn mocks base method. +func (m *MockAdministratorRoleRepository) 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 *MockAdministratorRoleRepositoryMockRecorder) InstanceIDColumn() *MockAdministratorRoleRepositoryInstanceIDColumnCall { + mr.mock.ctrl.T.Helper() + call := mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "InstanceIDColumn", reflect.TypeOf((*MockAdministratorRoleRepository)(nil).InstanceIDColumn)) + return &MockAdministratorRoleRepositoryInstanceIDColumnCall{Call: call} +} + +// MockAdministratorRoleRepositoryInstanceIDColumnCall wrap *gomock.Call +type MockAdministratorRoleRepositoryInstanceIDColumnCall struct { + *gomock.Call +} + +// Return rewrite *gomock.Call.Return +func (c *MockAdministratorRoleRepositoryInstanceIDColumnCall) Return(arg0 database.Column) *MockAdministratorRoleRepositoryInstanceIDColumnCall { + c.Call = c.Call.Return(arg0) + return c +} + +// Do rewrite *gomock.Call.Do +func (c *MockAdministratorRoleRepositoryInstanceIDColumnCall) Do(f func() database.Column) *MockAdministratorRoleRepositoryInstanceIDColumnCall { + c.Call = c.Call.Do(f) + return c +} + +// DoAndReturn rewrite *gomock.Call.DoAndReturn +func (c *MockAdministratorRoleRepositoryInstanceIDColumnCall) DoAndReturn(f func() database.Column) *MockAdministratorRoleRepositoryInstanceIDColumnCall { + c.Call = c.Call.DoAndReturn(f) + return c +} + +// InstanceIDCondition mocks base method. +func (m *MockAdministratorRoleRepository) 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 *MockAdministratorRoleRepositoryMockRecorder) InstanceIDCondition(instanceID any) *MockAdministratorRoleRepositoryInstanceIDConditionCall { + mr.mock.ctrl.T.Helper() + call := mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "InstanceIDCondition", reflect.TypeOf((*MockAdministratorRoleRepository)(nil).InstanceIDCondition), instanceID) + return &MockAdministratorRoleRepositoryInstanceIDConditionCall{Call: call} +} + +// MockAdministratorRoleRepositoryInstanceIDConditionCall wrap *gomock.Call +type MockAdministratorRoleRepositoryInstanceIDConditionCall struct { + *gomock.Call +} + +// Return rewrite *gomock.Call.Return +func (c *MockAdministratorRoleRepositoryInstanceIDConditionCall) Return(arg0 database.Condition) *MockAdministratorRoleRepositoryInstanceIDConditionCall { + c.Call = c.Call.Return(arg0) + return c +} + +// Do rewrite *gomock.Call.Do +func (c *MockAdministratorRoleRepositoryInstanceIDConditionCall) Do(f func(string) database.Condition) *MockAdministratorRoleRepositoryInstanceIDConditionCall { + c.Call = c.Call.Do(f) + return c +} + +// DoAndReturn rewrite *gomock.Call.DoAndReturn +func (c *MockAdministratorRoleRepositoryInstanceIDConditionCall) DoAndReturn(f func(string) database.Condition) *MockAdministratorRoleRepositoryInstanceIDConditionCall { + c.Call = c.Call.DoAndReturn(f) + return c +} + +// PermissionColumn mocks base method. +func (m *MockAdministratorRoleRepository) PermissionColumn() database.Column { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "PermissionColumn") + ret0, _ := ret[0].(database.Column) + return ret0 +} + +// PermissionColumn indicates an expected call of PermissionColumn. +func (mr *MockAdministratorRoleRepositoryMockRecorder) PermissionColumn() *MockAdministratorRoleRepositoryPermissionColumnCall { + mr.mock.ctrl.T.Helper() + call := mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "PermissionColumn", reflect.TypeOf((*MockAdministratorRoleRepository)(nil).PermissionColumn)) + return &MockAdministratorRoleRepositoryPermissionColumnCall{Call: call} +} + +// MockAdministratorRoleRepositoryPermissionColumnCall wrap *gomock.Call +type MockAdministratorRoleRepositoryPermissionColumnCall struct { + *gomock.Call +} + +// Return rewrite *gomock.Call.Return +func (c *MockAdministratorRoleRepositoryPermissionColumnCall) Return(arg0 database.Column) *MockAdministratorRoleRepositoryPermissionColumnCall { + c.Call = c.Call.Return(arg0) + return c +} + +// Do rewrite *gomock.Call.Do +func (c *MockAdministratorRoleRepositoryPermissionColumnCall) Do(f func() database.Column) *MockAdministratorRoleRepositoryPermissionColumnCall { + c.Call = c.Call.Do(f) + return c +} + +// DoAndReturn rewrite *gomock.Call.DoAndReturn +func (c *MockAdministratorRoleRepositoryPermissionColumnCall) DoAndReturn(f func() database.Column) *MockAdministratorRoleRepositoryPermissionColumnCall { + c.Call = c.Call.DoAndReturn(f) + return c +} + +// PermissionCondition mocks base method. +func (m *MockAdministratorRoleRepository) PermissionCondition(op database.TextOperation, permission string) database.Condition { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "PermissionCondition", op, permission) + ret0, _ := ret[0].(database.Condition) + return ret0 +} + +// PermissionCondition indicates an expected call of PermissionCondition. +func (mr *MockAdministratorRoleRepositoryMockRecorder) PermissionCondition(op, permission any) *MockAdministratorRoleRepositoryPermissionConditionCall { + mr.mock.ctrl.T.Helper() + call := mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "PermissionCondition", reflect.TypeOf((*MockAdministratorRoleRepository)(nil).PermissionCondition), op, permission) + return &MockAdministratorRoleRepositoryPermissionConditionCall{Call: call} +} + +// MockAdministratorRoleRepositoryPermissionConditionCall wrap *gomock.Call +type MockAdministratorRoleRepositoryPermissionConditionCall struct { + *gomock.Call +} + +// Return rewrite *gomock.Call.Return +func (c *MockAdministratorRoleRepositoryPermissionConditionCall) Return(arg0 database.Condition) *MockAdministratorRoleRepositoryPermissionConditionCall { + c.Call = c.Call.Return(arg0) + return c +} + +// Do rewrite *gomock.Call.Do +func (c *MockAdministratorRoleRepositoryPermissionConditionCall) Do(f func(database.TextOperation, string) database.Condition) *MockAdministratorRoleRepositoryPermissionConditionCall { + c.Call = c.Call.Do(f) + return c +} + +// DoAndReturn rewrite *gomock.Call.DoAndReturn +func (c *MockAdministratorRoleRepositoryPermissionConditionCall) DoAndReturn(f func(database.TextOperation, string) database.Condition) *MockAdministratorRoleRepositoryPermissionConditionCall { + c.Call = c.Call.DoAndReturn(f) + return c +} + +// PrimaryKeyColumns mocks base method. +func (m *MockAdministratorRoleRepository) 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 *MockAdministratorRoleRepositoryMockRecorder) PrimaryKeyColumns() *MockAdministratorRoleRepositoryPrimaryKeyColumnsCall { + mr.mock.ctrl.T.Helper() + call := mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "PrimaryKeyColumns", reflect.TypeOf((*MockAdministratorRoleRepository)(nil).PrimaryKeyColumns)) + return &MockAdministratorRoleRepositoryPrimaryKeyColumnsCall{Call: call} +} + +// MockAdministratorRoleRepositoryPrimaryKeyColumnsCall wrap *gomock.Call +type MockAdministratorRoleRepositoryPrimaryKeyColumnsCall struct { + *gomock.Call +} + +// Return rewrite *gomock.Call.Return +func (c *MockAdministratorRoleRepositoryPrimaryKeyColumnsCall) Return(arg0 []database.Column) *MockAdministratorRoleRepositoryPrimaryKeyColumnsCall { + c.Call = c.Call.Return(arg0) + return c +} + +// Do rewrite *gomock.Call.Do +func (c *MockAdministratorRoleRepositoryPrimaryKeyColumnsCall) Do(f func() []database.Column) *MockAdministratorRoleRepositoryPrimaryKeyColumnsCall { + c.Call = c.Call.Do(f) + return c +} + +// DoAndReturn rewrite *gomock.Call.DoAndReturn +func (c *MockAdministratorRoleRepositoryPrimaryKeyColumnsCall) DoAndReturn(f func() []database.Column) *MockAdministratorRoleRepositoryPrimaryKeyColumnsCall { + c.Call = c.Call.DoAndReturn(f) + return c +} + +// PrimaryKeyCondition mocks base method. +func (m *MockAdministratorRoleRepository) PrimaryKeyCondition(instanceID, roleName, permission string) database.Condition { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "PrimaryKeyCondition", instanceID, roleName, permission) + ret0, _ := ret[0].(database.Condition) + return ret0 +} + +// PrimaryKeyCondition indicates an expected call of PrimaryKeyCondition. +func (mr *MockAdministratorRoleRepositoryMockRecorder) PrimaryKeyCondition(instanceID, roleName, permission any) *MockAdministratorRoleRepositoryPrimaryKeyConditionCall { + mr.mock.ctrl.T.Helper() + call := mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "PrimaryKeyCondition", reflect.TypeOf((*MockAdministratorRoleRepository)(nil).PrimaryKeyCondition), instanceID, roleName, permission) + return &MockAdministratorRoleRepositoryPrimaryKeyConditionCall{Call: call} +} + +// MockAdministratorRoleRepositoryPrimaryKeyConditionCall wrap *gomock.Call +type MockAdministratorRoleRepositoryPrimaryKeyConditionCall struct { + *gomock.Call +} + +// Return rewrite *gomock.Call.Return +func (c *MockAdministratorRoleRepositoryPrimaryKeyConditionCall) Return(arg0 database.Condition) *MockAdministratorRoleRepositoryPrimaryKeyConditionCall { + c.Call = c.Call.Return(arg0) + return c +} + +// Do rewrite *gomock.Call.Do +func (c *MockAdministratorRoleRepositoryPrimaryKeyConditionCall) Do(f func(string, string, string) database.Condition) *MockAdministratorRoleRepositoryPrimaryKeyConditionCall { + c.Call = c.Call.Do(f) + return c +} + +// DoAndReturn rewrite *gomock.Call.DoAndReturn +func (c *MockAdministratorRoleRepositoryPrimaryKeyConditionCall) DoAndReturn(f func(string, string, string) database.Condition) *MockAdministratorRoleRepositoryPrimaryKeyConditionCall { + c.Call = c.Call.DoAndReturn(f) + return c +} + +// RemovePermissions mocks base method. +func (m *MockAdministratorRoleRepository) RemovePermissions(ctx context.Context, client database.QueryExecutor, instanceID, role string, permissionsToRemove ...string) (int64, error) { + m.ctrl.T.Helper() + varargs := []any{ctx, client, instanceID, role} + for _, a := range permissionsToRemove { + varargs = append(varargs, a) + } + ret := m.ctrl.Call(m, "RemovePermissions", varargs...) + ret0, _ := ret[0].(int64) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// RemovePermissions indicates an expected call of RemovePermissions. +func (mr *MockAdministratorRoleRepositoryMockRecorder) RemovePermissions(ctx, client, instanceID, role any, permissionsToRemove ...any) *MockAdministratorRoleRepositoryRemovePermissionsCall { + mr.mock.ctrl.T.Helper() + varargs := append([]any{ctx, client, instanceID, role}, permissionsToRemove...) + call := mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "RemovePermissions", reflect.TypeOf((*MockAdministratorRoleRepository)(nil).RemovePermissions), varargs...) + return &MockAdministratorRoleRepositoryRemovePermissionsCall{Call: call} +} + +// MockAdministratorRoleRepositoryRemovePermissionsCall wrap *gomock.Call +type MockAdministratorRoleRepositoryRemovePermissionsCall struct { + *gomock.Call +} + +// Return rewrite *gomock.Call.Return +func (c *MockAdministratorRoleRepositoryRemovePermissionsCall) Return(arg0 int64, arg1 error) *MockAdministratorRoleRepositoryRemovePermissionsCall { + c.Call = c.Call.Return(arg0, arg1) + return c +} + +// Do rewrite *gomock.Call.Do +func (c *MockAdministratorRoleRepositoryRemovePermissionsCall) Do(f func(context.Context, database.QueryExecutor, string, string, ...string) (int64, error)) *MockAdministratorRoleRepositoryRemovePermissionsCall { + c.Call = c.Call.Do(f) + return c +} + +// DoAndReturn rewrite *gomock.Call.DoAndReturn +func (c *MockAdministratorRoleRepositoryRemovePermissionsCall) DoAndReturn(f func(context.Context, database.QueryExecutor, string, string, ...string) (int64, error)) *MockAdministratorRoleRepositoryRemovePermissionsCall { + c.Call = c.Call.DoAndReturn(f) + return c +} + +// RoleNameColumn mocks base method. +func (m *MockAdministratorRoleRepository) RoleNameColumn() database.Column { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "RoleNameColumn") + ret0, _ := ret[0].(database.Column) + return ret0 +} + +// RoleNameColumn indicates an expected call of RoleNameColumn. +func (mr *MockAdministratorRoleRepositoryMockRecorder) RoleNameColumn() *MockAdministratorRoleRepositoryRoleNameColumnCall { + mr.mock.ctrl.T.Helper() + call := mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "RoleNameColumn", reflect.TypeOf((*MockAdministratorRoleRepository)(nil).RoleNameColumn)) + return &MockAdministratorRoleRepositoryRoleNameColumnCall{Call: call} +} + +// MockAdministratorRoleRepositoryRoleNameColumnCall wrap *gomock.Call +type MockAdministratorRoleRepositoryRoleNameColumnCall struct { + *gomock.Call +} + +// Return rewrite *gomock.Call.Return +func (c *MockAdministratorRoleRepositoryRoleNameColumnCall) Return(arg0 database.Column) *MockAdministratorRoleRepositoryRoleNameColumnCall { + c.Call = c.Call.Return(arg0) + return c +} + +// Do rewrite *gomock.Call.Do +func (c *MockAdministratorRoleRepositoryRoleNameColumnCall) Do(f func() database.Column) *MockAdministratorRoleRepositoryRoleNameColumnCall { + c.Call = c.Call.Do(f) + return c +} + +// DoAndReturn rewrite *gomock.Call.DoAndReturn +func (c *MockAdministratorRoleRepositoryRoleNameColumnCall) DoAndReturn(f func() database.Column) *MockAdministratorRoleRepositoryRoleNameColumnCall { + c.Call = c.Call.DoAndReturn(f) + return c +} + +// RoleNameCondition mocks base method. +func (m *MockAdministratorRoleRepository) RoleNameCondition(op database.TextOperation, name string) database.Condition { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "RoleNameCondition", op, name) + ret0, _ := ret[0].(database.Condition) + return ret0 +} + +// RoleNameCondition indicates an expected call of RoleNameCondition. +func (mr *MockAdministratorRoleRepositoryMockRecorder) RoleNameCondition(op, name any) *MockAdministratorRoleRepositoryRoleNameConditionCall { + mr.mock.ctrl.T.Helper() + call := mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "RoleNameCondition", reflect.TypeOf((*MockAdministratorRoleRepository)(nil).RoleNameCondition), op, name) + return &MockAdministratorRoleRepositoryRoleNameConditionCall{Call: call} +} + +// MockAdministratorRoleRepositoryRoleNameConditionCall wrap *gomock.Call +type MockAdministratorRoleRepositoryRoleNameConditionCall struct { + *gomock.Call +} + +// Return rewrite *gomock.Call.Return +func (c *MockAdministratorRoleRepositoryRoleNameConditionCall) Return(arg0 database.Condition) *MockAdministratorRoleRepositoryRoleNameConditionCall { + c.Call = c.Call.Return(arg0) + return c +} + +// Do rewrite *gomock.Call.Do +func (c *MockAdministratorRoleRepositoryRoleNameConditionCall) Do(f func(database.TextOperation, string) database.Condition) *MockAdministratorRoleRepositoryRoleNameConditionCall { + c.Call = c.Call.Do(f) + return c +} + +// DoAndReturn rewrite *gomock.Call.DoAndReturn +func (c *MockAdministratorRoleRepositoryRoleNameConditionCall) DoAndReturn(f func(database.TextOperation, string) database.Condition) *MockAdministratorRoleRepositoryRoleNameConditionCall { + c.Call = c.Call.DoAndReturn(f) + return c +} diff --git a/backend/v3/storage/database/dialect/postgres/migration/018_administrator_role_permissions_table.go b/backend/v3/storage/database/dialect/postgres/migration/018_administrator_role_permissions_table.go new file mode 100644 index 000000000..ed8612bea --- /dev/null +++ b/backend/v3/storage/database/dialect/postgres/migration/018_administrator_role_permissions_table.go @@ -0,0 +1,16 @@ +package migration + +import ( + _ "embed" +) + +var ( + //go:embed 018_administrator_role_permissions_table/up.sql + up018AdministratorRolePermissionsTable string + //go:embed 018_administrator_role_permissions_table/down.sql + down018AdministratorRolePermissionsTable string +) + +func init() { + registerSQLMigration(18, up018AdministratorRolePermissionsTable, down018AdministratorRolePermissionsTable) +} diff --git a/backend/v3/storage/database/dialect/postgres/migration/018_administrator_role_permissions_table/down.sql b/backend/v3/storage/database/dialect/postgres/migration/018_administrator_role_permissions_table/down.sql new file mode 100644 index 000000000..25ad37b0f --- /dev/null +++ b/backend/v3/storage/database/dialect/postgres/migration/018_administrator_role_permissions_table/down.sql @@ -0,0 +1 @@ +DROP TABLE IF EXISTS zitadel.administrator_role_permissions; diff --git a/backend/v3/storage/database/dialect/postgres/migration/018_administrator_role_permissions_table/up.sql b/backend/v3/storage/database/dialect/postgres/migration/018_administrator_role_permissions_table/up.sql new file mode 100644 index 000000000..59aa5f891 --- /dev/null +++ b/backend/v3/storage/database/dialect/postgres/migration/018_administrator_role_permissions_table/up.sql @@ -0,0 +1,8 @@ +CREATE TABLE zitadel.administrator_role_permissions( + instance_id TEXT NOT NULL + , role_name TEXT NOT NULL CHECK (role_name <> '') + , permission TEXT NOT NULL CHECK (permission <> '') + + , PRIMARY KEY (instance_id, permission, role_name) + , FOREIGN KEY (instance_id) REFERENCES zitadel.instances(id) ON DELETE CASCADE +); diff --git a/backend/v3/storage/database/repository/administrator_role.go b/backend/v3/storage/database/repository/administrator_role.go new file mode 100644 index 000000000..43a053c41 --- /dev/null +++ b/backend/v3/storage/database/repository/administrator_role.go @@ -0,0 +1,97 @@ +package repository + +import ( + "context" + + "github.com/zitadel/zitadel/backend/v3/domain" + "github.com/zitadel/zitadel/backend/v3/storage/database" +) + +var _ domain.AdministratorRoleRepository = (*administratorRole)(nil) + +type administratorRole struct{} + +func AdministratorRoleRepository() domain.AdministratorRoleRepository { + return new(administratorRole) +} + +func (administratorRole) unqualifiedTableName() string { + return "administrator_role_permissions" +} + +func (a administratorRole) qualifiedTableName() string { + return "zitadel." + a.unqualifiedTableName() +} + +// AddPermission implements [domain.AdministratorRoleRepository]. +func (a administratorRole) AddPermissions(ctx context.Context, client database.QueryExecutor, instanceID, role string, permissions ...string) (int64, error) { + if len(permissions) == 0 { + return 0, database.ErrNoChanges + } + builder := database.NewStatementBuilder( + "INSERT INTO zitadel.administrator_role_permissions (instance_id, role_name, permission)"+ + " SELECT $1::text, $2::text, unnest($3::text[])"+ + " ON CONFLICT (instance_id, permission, role_name) DO NOTHING", instanceID, role, permissions, + ) + return client.Exec(ctx, builder.String(), builder.Args()...) +} + +// RemovePermissions implements [domain.AdministratorRoleRepository]. +func (a administratorRole) RemovePermissions(ctx context.Context, client database.QueryExecutor, instanceID, role string, permissionsToRemove ...string) (int64, error) { + if len(permissionsToRemove) == 0 { + return 0, database.ErrNoChanges + } + builder := database.NewStatementBuilder( + "DELETE FROM zitadel.administrator_role_permissions"+ + " WHERE instance_id = $1 AND role_name = $2 AND permission = ANY($3::text[])", instanceID, role, permissionsToRemove, + ) + return client.Exec(ctx, builder.String(), builder.Args()...) +} + +// PrimaryKeyCondition implements [domain.AdministratorRoleRepository]. +func (a administratorRole) PrimaryKeyCondition(instanceID, roleName, permission string) database.Condition { + return database.And( + a.InstanceIDCondition(instanceID), + a.RoleNameCondition(database.TextOperationEqual, roleName), + a.PermissionCondition(database.TextOperationEqual, permission), + ) +} + +// InstanceIDCondition implements [domain.AdministratorRoleRepository]. +func (a administratorRole) InstanceIDCondition(instanceID string) database.Condition { + return database.NewTextCondition(a.InstanceIDColumn(), database.TextOperationEqual, instanceID) +} + +// RoleNameCondition implements [domain.AdministratorRoleRepository]. +func (a administratorRole) RoleNameCondition(op database.TextOperation, name string) database.Condition { + return database.NewTextCondition(a.RoleNameColumn(), op, name) +} + +// PermissionCondition implements [domain.AdministratorRoleRepository]. +func (a administratorRole) PermissionCondition(op database.TextOperation, permission string) database.Condition { + return database.NewTextCondition(a.PermissionColumn(), op, permission) +} + +// PrimaryKeyColumns implements [domain.AdministratorRoleRepository]. +func (a administratorRole) PrimaryKeyColumns() []database.Column { + return []database.Column{ + a.InstanceIDColumn(), + a.PermissionColumn(), + a.RoleNameColumn(), + } +} + +// RoleNameColumn implements [domain.AdministratorRoleRepository]. +func (a administratorRole) RoleNameColumn() database.Column { + return database.NewColumn(a.unqualifiedTableName(), "role_name") +} + +// PermissionColumn implements [domain.AdministratorRoleRepository]. +func (a administratorRole) PermissionColumn() database.Column { + return database.NewColumn(a.unqualifiedTableName(), "permission") +} + +// InstanceIDColumn implements [domain.AdministratorRoleRepository]. +func (a administratorRole) InstanceIDColumn() database.Column { + return database.NewColumn(a.unqualifiedTableName(), "instance_id") +} diff --git a/backend/v3/storage/database/repository/administrator_role_test.go b/backend/v3/storage/database/repository/administrator_role_test.go new file mode 100644 index 000000000..48ae369fd --- /dev/null +++ b/backend/v3/storage/database/repository/administrator_role_test.go @@ -0,0 +1,200 @@ +package repository_test + +import ( + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + + "github.com/zitadel/zitadel/backend/v3/storage/database" + "github.com/zitadel/zitadel/backend/v3/storage/database/repository" +) + +type administratorRolePermissionRow struct { + InstanceID string `db:"instance_id"` + RoleName string `db:"role_name"` + Permission string `db:"permission"` +} + +func TestAdministratorRoleRepository_AddPermissions(t *testing.T) { + tx, rollback := transactionForRollback(t) + defer rollback() + + instanceID := createInstance(t, tx) + repo := repository.AdministratorRoleRepository() + + tests := []struct { + name string + role string + permissions []string + wantRows []administratorRolePermissionRow + wantErr error + wantCount int64 + }{ + { + name: "add deduplicated permissions", + role: "ORG_OWNER", + permissions: []string{"org.read", "org.write", "org.read"}, + wantCount: 2, + wantRows: []administratorRolePermissionRow{ + {InstanceID: instanceID, RoleName: "ORG_OWNER", Permission: "org.read"}, + {InstanceID: instanceID, RoleName: "ORG_OWNER", Permission: "org.write"}, + }, + }, + { + name: "no permissions", + role: "ORG_OWNER", + wantErr: database.ErrNoChanges, + wantCount: 0, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + savepoint, rollbackSavepoint := savepointForRollback(t, tx) + defer rollbackSavepoint() + + count, err := repo.AddPermissions(t.Context(), savepoint, instanceID, tt.role, tt.permissions...) + require.ErrorIs(t, err, tt.wantErr) + assert.Equal(t, tt.wantCount, count) + + if tt.wantErr == nil { + assert.Equal(t, + tt.wantRows, + listAdministratorRolePermissions(t, savepoint, repo.RoleNameCondition(database.TextOperationEqual, tt.role)), + ) + } + }) + } +} + +func TestAdministratorRoleRepository_RemovePermissions(t *testing.T) { + tx, rollback := transactionForRollback(t) + defer rollback() + + instanceID := createInstance(t, tx) + + repo := repository.AdministratorRoleRepository() + count, err := repo.AddPermissions(t.Context(), tx, instanceID, "INSTANCE_OWNER", "instance.read", "instance.write") + require.NoError(t, err) + assert.Equal(t, int64(2), count) + + tests := []struct { + name string + role string + permissions []string + wantRows []administratorRolePermissionRow + wantErr error + wantCount int64 + }{ + { + name: "no permissions", + role: "INSTANCE_OWNER", + wantErr: database.ErrNoChanges, + }, + { + name: "remove permissions", + role: "INSTANCE_OWNER", + permissions: []string{"instance.read"}, + wantCount: 1, + wantRows: []administratorRolePermissionRow{ + {InstanceID: instanceID, RoleName: "INSTANCE_OWNER", Permission: "instance.write"}, + }, + }, + { + name: "remove exact pair and missing values", + role: "INSTANCE_OWNER", + permissions: []string{"instance.write", "missing"}, + wantCount: 1, + wantRows: []administratorRolePermissionRow{{InstanceID: instanceID, RoleName: "INSTANCE_OWNER", Permission: "instance.read"}}, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + savepoint, rollbackSavepoint := savepointForRollback(t, tx) + defer rollbackSavepoint() + + count, err = repo.RemovePermissions(t.Context(), savepoint, instanceID, tt.role, tt.permissions...) + require.ErrorIs(t, err, tt.wantErr) + assert.Equal(t, tt.wantCount, count) + if tt.wantErr == nil { + assert.Equal(t, + tt.wantRows, + listAdministratorRolePermissions(t, savepoint, repo.RoleNameCondition(database.TextOperationEqual, "INSTANCE_OWNER")), + ) + } + }) + } +} + +func TestAdministratorRoleRepository_AddAndRemoveAcrossCalls(t *testing.T) { + tx, rollback := transactionForRollback(t) + defer rollback() + + instanceID := createInstance(t, tx) + + repo := repository.AdministratorRoleRepository() + + count, err := repo.AddPermissions(t.Context(), tx, instanceID, "INSTANCE_OWNER", "instance.read", "instance.write") + require.NoError(t, err) + assert.Equal(t, int64(2), count) + + count, err = repo.AddPermissions(t.Context(), tx, instanceID, "INSTANCE_OWNER", "instance.manage", "instance.audit", "instance.write") + require.NoError(t, err) + assert.Equal(t, int64(2), count) + + count, err = repo.RemovePermissions(t.Context(), tx, instanceID, "INSTANCE_OWNER", "instance.read") + require.NoError(t, err) + assert.Equal(t, int64(1), count) + + assert.Equal(t, + []administratorRolePermissionRow{ + {InstanceID: instanceID, RoleName: "INSTANCE_OWNER", Permission: "instance.audit"}, + {InstanceID: instanceID, RoleName: "INSTANCE_OWNER", Permission: "instance.manage"}, + {InstanceID: instanceID, RoleName: "INSTANCE_OWNER", Permission: "instance.write"}, + }, + listAdministratorRolePermissions(t, tx, repo.RoleNameCondition(database.TextOperationEqual, "INSTANCE_OWNER")), + ) +} + +func TestAdministratorRoleRepository_PrimaryKeyCondition(t *testing.T) { + tx, rollback := transactionForRollback(t) + defer rollback() + + instanceID := createInstance(t, tx) + + repo := repository.AdministratorRoleRepository() + count, err := repo.AddPermissions(t.Context(), tx, instanceID, "PROJECT_OWNER", "project.read", "project.write") + require.NoError(t, err) + assert.Equal(t, int64(2), count) + + rows := listAdministratorRolePermissions(t, tx, repo.PrimaryKeyCondition(instanceID, "PROJECT_OWNER", "project.read")) + assert.Equal(t, + []administratorRolePermissionRow{{InstanceID: instanceID, RoleName: "PROJECT_OWNER", Permission: "project.read"}}, + rows, + ) +} + +func listAdministratorRolePermissions(t *testing.T, tx database.QueryExecutor, condition database.Condition) []administratorRolePermissionRow { + t.Helper() + + builder := database.NewStatementBuilder(`SELECT instance_id, role_name, permission FROM zitadel.administrator_role_permissions`) + if condition != nil { + builder.WriteString(" WHERE ") + condition.Write(builder) + } + builder.WriteString(" ORDER BY instance_id, role_name, permission") + + rows, err := tx.Query(t.Context(), builder.String(), builder.Args()...) + require.NoError(t, err) + + var result []*administratorRolePermissionRow + require.NoError(t, rows.(database.CollectableRows).Collect(&result)) + + out := make([]administratorRolePermissionRow, len(result)) + for i, row := range result { + out[i] = *row + } + return out +} diff --git a/internal/api/grpc/instance/v2/integration_test/domain_test.go b/internal/api/grpc/instance/v2/integration_test/domain_test.go index 93868e6a7..110ef5123 100644 --- a/internal/api/grpc/instance/v2/integration_test/domain_test.go +++ b/internal/api/grpc/instance/v2/integration_test/domain_test.go @@ -43,6 +43,9 @@ func TestAddCustomDomain(t *testing.T) { relationalInst.Client.InstanceV2.DeleteInstance(ctxWithSysAuthZ, &instance.DeleteInstanceRequest{InstanceId: relationalInst.ID()}) }) + // ugly fix to wait until instances are projected in all read models. + time.Sleep(10 * time.Second) + type instanceAndCtx struct { testType string instance *integration.Instance diff --git a/internal/query/projection/administrator_role_permission_relational.go b/internal/query/projection/administrator_role_permission_relational.go new file mode 100644 index 000000000..3399c0759 --- /dev/null +++ b/internal/query/projection/administrator_role_permission_relational.go @@ -0,0 +1,45 @@ +package projection + +import ( + "context" + "database/sql" + + 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/permission" + "github.com/zitadel/zitadel/internal/zerrors" +) + +func (p *relationalTablesProjection) reduceAdministratorRolePermissionAdded(event eventstore.Event) (*handler.Statement, error) { + e, err := assertEvent[*permission.AddedEvent](event) + if err != nil { + return nil, err + } + return handler.NewStatement(e, func(ctx context.Context, ex handler.Executer, _ string) error { + tx, ok := ex.(*sql.Tx) + if !ok { + return zerrors.ThrowInvalidArgumentf(nil, "HANDL-v3ARP01", "reduce.wrong.db.pool %T", ex) + } + repo := repository.AdministratorRoleRepository() + _, err := repo.AddPermissions(ctx, v3_sql.SQLTx(tx), e.Aggregate().InstanceID, e.Role, e.Permission) + return err + }), nil +} + +func (p *relationalTablesProjection) reduceAdministratorRolePermissionRemoved(event eventstore.Event) (*handler.Statement, error) { + e, err := assertEvent[*permission.RemovedEvent](event) + if err != nil { + return nil, err + } + return handler.NewStatement(e, func(ctx context.Context, ex handler.Executer, _ string) error { + tx, ok := ex.(*sql.Tx) + if !ok { + return zerrors.ThrowInvalidArgumentf(nil, "HANDL-v3ARP02", "reduce.wrong.db.pool %T", ex) + } + repo := repository.AdministratorRoleRepository() + _, err := repo.RemovePermissions(ctx, v3_sql.SQLTx(tx), e.Aggregate().InstanceID, e.Role, e.Permission) + return err + }), nil +} diff --git a/internal/query/projection/administrator_role_permission_relational_test.go b/internal/query/projection/administrator_role_permission_relational_test.go new file mode 100644 index 000000000..563eaac97 --- /dev/null +++ b/internal/query/projection/administrator_role_permission_relational_test.go @@ -0,0 +1,94 @@ +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/permission" +) + +type administratorRolePermission struct { + RoleName string `db:"role_name"` + Permission string `db:"permission"` +} + +func TestAdministratorRolePermissionReducers(t *testing.T) { + handler := &relationalTablesProjection{} + rawTx, tx := getTransactions(t) + t.Cleanup(func() { + require.NoError(t, rawTx.Rollback()) + }) + + instanceID := seedAdministratorRolePermissions(t, tx) + + repo := repository.AdministratorRoleRepository() + + t.Run("added event inserts a permission row", func(t *testing.T) { + event := permission.NewAddedEvent(t.Context(), permission.NewAggregate(instanceID), "ORG_OWNER", "org.read") + require.True(t, callReduce(t, rawTx, handler, event)) + assert.Equal(t, + []administratorRolePermission{{RoleName: "ORG_OWNER", Permission: "org.read"}}, + listReducedAdministratorRolePermissions(t, tx), + ) + }) + + t.Run("removed event deletes only the matching permission row", func(t *testing.T) { + _, err := repo.AddPermissions(t.Context(), tx, instanceID, "INSTANCE_OWNER", "instance.read", "instance.write") + require.NoError(t, err) + + event := permission.NewRemovedEvent(t.Context(), permission.NewAggregate(instanceID), "INSTANCE_OWNER", "instance.read") + require.True(t, callReduce(t, rawTx, handler, event)) + assert.Equal(t, + []administratorRolePermission{{RoleName: "INSTANCE_OWNER", Permission: "instance.write"}}, + listReducedAdministratorRolePermissions(t, tx, repo.RoleNameCondition(database.TextOperationEqual, "INSTANCE_OWNER")), + ) + }) +} + +func listReducedAdministratorRolePermissions(t *testing.T, tx database.QueryExecutor, conditions ...database.Condition) []administratorRolePermission { + t.Helper() + + builder := database.NewStatementBuilder(`SELECT role_name, permission FROM zitadel.administrator_role_permissions`) + if len(conditions) > 0 { + builder.WriteString(" WHERE ") + database.And(conditions...).Write(builder) + } + builder.WriteString(" ORDER BY role_name, permission") + + rows, err := tx.Query(t.Context(), builder.String(), builder.Args()...) + require.NoError(t, err) + + var result []*administratorRolePermission + require.NoError(t, rows.(database.CollectableRows).Collect(&result)) + + out := make([]administratorRolePermission, len(result)) + for i, row := range result { + out[i] = *row + } + return out +} + +func seedAdministratorRolePermissions(t *testing.T, tx database.QueryExecutor) string { + t.Helper() + + instanceID := fmt.Sprintf("instance-%d", time.Now().UnixNano()) + err := repository.InstanceRepository().Create(t.Context(), tx, &repoDomain.Instance{ + ID: instanceID, + Name: "instance", + DefaultOrgID: "default-org", + IAMProjectID: "iam-project", + ConsoleClientID: "console-client", + ConsoleAppID: "console-app", + DefaultLanguage: "en", + }) + require.NoError(t, err) + + return instanceID +} diff --git a/internal/query/projection/relational_table.go b/internal/query/projection/relational_table.go index 5d4f03748..d71a446e7 100644 --- a/internal/query/projection/relational_table.go +++ b/internal/query/projection/relational_table.go @@ -8,6 +8,7 @@ import ( "github.com/zitadel/zitadel/internal/repository/instance" "github.com/zitadel/zitadel/internal/repository/org" settings "github.com/zitadel/zitadel/internal/repository/organization_settings" + "github.com/zitadel/zitadel/internal/repository/permission" "github.com/zitadel/zitadel/internal/repository/project" "github.com/zitadel/zitadel/internal/repository/session" "github.com/zitadel/zitadel/internal/repository/user" @@ -26,6 +27,19 @@ func (*relationalTablesProjection) Name() string { func (p *relationalTablesProjection) Reducers() []handler.AggregateReducer { return []handler.AggregateReducer{ + { + Aggregate: permission.AggregateType, + EventReducers: []handler.EventReducer{ + { + Event: permission.AddedType, + Reduce: p.reduceAdministratorRolePermissionAdded, + }, + { + Event: permission.RemovedType, + Reduce: p.reduceAdministratorRolePermissionRemoved, + }, + }, + }, { Aggregate: instance.AggregateType, EventReducers: []handler.EventReducer{