feat: add administrator role permission table (#11817)

This update introduces a new table for managing administrator role
permissions, along with the necessary repository and projection logic to
handle adding and removing permissions. It also includes tests to ensure
the correct functionality of the permission management system.

closes https://github.com/zitadel/zitadel/issues/10219

---------

Co-authored-by: Marco Ardizzone <[email protected]>
This commit is contained in:
Silvan
2026-04-14 06:01:52 +00:00
committed by GitHub
co-authored by Marco Ardizzone
parent bb3b52dda3
commit 7b9d2b7d17
11 changed files with 944 additions and 0 deletions
+32
View File
@@ -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)
}
@@ -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
}
@@ -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)
}
@@ -0,0 +1 @@
DROP TABLE IF EXISTS zitadel.administrator_role_permissions;
@@ -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
);
@@ -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")
}
@@ -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
}
@@ -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
@@ -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
}
@@ -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
}
@@ -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{