MM-68830: Preserve unknown permissions during migrations on downgrade (#36888)

* MM-68830: Preserve unknown permissions during migrations on downgrade

A server that was upgraded to a newer release (which introduced new
permissions and wrote them into roles) and then downgraded fails fatally
at startup: the permissions migration re-saves every role, and
Role.Save() rejects any permission the older binary does not recognize,
making the downgrade unrecoverable.

Add RoleStore.SavePreservingUnknownPermissions, used only by
doPermissionsMigration, which tolerates and preserves permissions this
build does not recognize (logging a warning) instead of rejecting the
role. The regular Save() — and therefore the role API path — stays
strict, so unknown permissions cannot be introduced through user input.

Unrecognized permissions are kept on disk so they are not lost on a
later re-upgrade.

* MM-68830: assert save forwarding in role cache tests

Address review feedback: assert the underlying store's Save and
SavePreservingUnknownPermissions are actually invoked (the cache
invalidation defer fires regardless of forwarding), and check the
returned errors.

* MM-68830: address review feedback

- Shorten log message in validateForSave
- Rename validationRole -> roleCopy for clarity
- Trim doc comments to describe behavior only
- List all unknown permissions in IsValidWithoutId error
- Assert specific error type in storetest

* MM-68830: add Role.Clone and use it in validateForSave

* MM-68830: add tests for Role.Clone

* MM-68830: fix scheme id deep copy assertion in Role.Clone test
This commit is contained in:
Felipe Martin
2026-06-09 11:18:19 +02:00
committed by GitHub
parent f6e7e71695
commit 755925fb73
14 changed files with 410 additions and 14 deletions
@@ -237,7 +237,10 @@ func (s *Server) doPermissionsMigration(key string, migrationMap permissionsMap,
for _, role := range roles {
role.Permissions = applyPermissionsMap(role, roleMap, migrationMap)
if _, err := s.Store().Role().Save(role); err != nil {
// Use SavePreservingUnknownPermissions so a server that was downgraded from a
// newer release (which wrote permissions this binary doesn't recognize) does
// not fail fatally here. Unknown permissions are logged and preserved (MM-68830).
if _, err := s.Store().Role().SavePreservingUnknownPermissions(role); err != nil {
var invErr *store.ErrInvalidInput
switch {
case errors.As(err, &invErr):
@@ -4,6 +4,7 @@
package app
import (
"slices"
"testing"
"github.com/stretchr/testify/assert"
@@ -48,7 +49,7 @@ func TestRestoreManageOAuthPermissionMigration(t *testing.T) {
return system.Name == model.MigrationKeyRestoreManageOAuthPermission && system.Value == "true"
})).Return(nil).Once()
roleStore.On("Save", mock.AnythingOfType("*model.Role")).
roleStore.On("SavePreservingUnknownPermissions", mock.AnythingOfType("*model.Role")).
Return(func(role *model.Role) *model.Role { return role }, nil).Twice()
appErr := th.App.Srv().doPermissionsMigration(model.MigrationKeyRestoreManageOAuthPermission, migrationMap, roles)
@@ -61,7 +62,7 @@ func TestRestoreManageOAuthPermissionMigration(t *testing.T) {
require.Nil(t, appErr)
assert.Len(t, systemAdminRole.Permissions, 2)
roleStore.AssertNumberOfCalls(t, "Save", 2)
roleStore.AssertNumberOfCalls(t, "SavePreservingUnknownPermissions", 2)
systemStore.AssertNumberOfCalls(t, "SaveOrUpdate", 1)
}
@@ -98,7 +99,7 @@ func TestAddManageAgentPermissionsMigration(t *testing.T) {
return system.Name == model.MigrationKeyAddManageAgentPermissions && system.Value == "true"
})).Return(nil).Once()
roleStore.On("Save", mock.AnythingOfType("*model.Role")).
roleStore.On("SavePreservingUnknownPermissions", mock.AnythingOfType("*model.Role")).
Return(func(role *model.Role) *model.Role { return role }, nil).Twice()
appErr := th.App.Srv().doPermissionsMigration(model.MigrationKeyAddManageAgentPermissions, migrationMap, roles)
@@ -115,10 +116,52 @@ func TestAddManageAgentPermissionsMigration(t *testing.T) {
assert.Len(t, systemAdminRole.Permissions, 3, "system_admin should still have 3 permissions after idempotent run")
assert.Len(t, systemUserRole.Permissions, 2, "system_user should still have 2 permissions after idempotent run")
roleStore.AssertNumberOfCalls(t, "Save", 2)
roleStore.AssertNumberOfCalls(t, "SavePreservingUnknownPermissions", 2)
systemStore.AssertNumberOfCalls(t, "SaveOrUpdate", 1)
}
// TestPermissionsMigrationPreservesUnknownPermissions is the regression test for
// MM-68830: a server downgraded from a newer release holds permissions the older
// binary does not recognize. The permissions migration must not fail fatally, and
// must preserve those unknown permissions rather than stripping them.
func TestPermissionsMigrationPreservesUnknownPermissions(t *testing.T) {
mainHelper.Parallel(t)
th := SetupWithStoreMock(t)
migrationMap, err := th.App.getAddManageAgentPermissionsMigration()
require.NoError(t, err)
const unknownPermission = "manage_own_agent_from_the_future"
systemAdminRole := &model.Role{
Name: model.SystemAdminRoleId,
Permissions: []string{model.PermissionManageSystem.Id, unknownPermission},
}
roles := []*model.Role{systemAdminRole}
mockStore := th.App.Srv().Store().(*mocks.Store)
roleStore := mocks.RoleStore{}
systemStore := mocks.SystemStore{}
mockStore.On("Role").Return(&roleStore)
mockStore.On("System").Return(&systemStore)
systemStore.On("GetByName", model.MigrationKeyAddManageAgentPermissions).
Return(nil, model.NewAppError("test", "missing", nil, "", 404)).Once()
systemStore.On("SaveOrUpdate", mock.AnythingOfType("*model.System")).Return(nil).Once()
// The migration must route through the tolerant save, and the unknown permission
// must still be present on the role handed to the store (not stripped).
roleStore.On("SavePreservingUnknownPermissions", mock.MatchedBy(func(role *model.Role) bool {
return role.Name == model.SystemAdminRoleId && slices.Contains(role.Permissions, unknownPermission)
})).Return(func(role *model.Role) *model.Role { return role }, nil).Once()
appErr := th.App.Srv().doPermissionsMigration(model.MigrationKeyAddManageAgentPermissions, migrationMap, roles)
require.Nil(t, appErr, "downgrade migration must not fail fatally on unknown permissions")
assert.Contains(t, systemAdminRole.Permissions, unknownPermission, "unknown permission must be preserved across the migration")
roleStore.AssertNumberOfCalls(t, "SavePreservingUnknownPermissions", 1)
}
func TestApplyPermissionsMap(t *testing.T) {
mainHelper.Parallel(t)
tt := []struct {
@@ -54,6 +54,7 @@ func getMockStore(t *testing.T) *mocks.Store {
fakeRole2 := model.Role{Id: "456", Name: "role-name2"}
mockRolesStore := mocks.RoleStore{}
mockRolesStore.On("Save", &fakeRole).Return(&model.Role{}, nil)
mockRolesStore.On("SavePreservingUnknownPermissions", &fakeRole).Return(&model.Role{}, nil)
mockRolesStore.On("Delete", "123").Return(&fakeRole, nil)
mockRolesStore.On("GetByName", context.Background(), "role-name").Return(&fakeRole, nil)
mockRolesStore.On("GetByNames", []string{"role-name"}).Return([]*model.Role{&fakeRole}, nil)
@@ -44,6 +44,14 @@ func (s LocalCacheRoleStore) Save(role *model.Role) (*model.Role, error) {
return s.RoleStore.Save(role)
}
func (s LocalCacheRoleStore) SavePreservingUnknownPermissions(role *model.Role) (*model.Role, error) {
if role.Name != "" {
defer s.rootStore.doInvalidateCacheCluster(s.rootStore.roleCache, role.Name, nil)
defer s.rootStore.doClearCacheCluster(s.rootStore.rolePermissionsCache)
}
return s.RoleStore.SavePreservingUnknownPermissions(role)
}
func (s LocalCacheRoleStore) GetByName(ctx context.Context, name string) (*model.Role, error) {
var role *model.Role
if err := s.rootStore.doStandardReadCache(s.rootStore.roleCache, name, &role); err == nil {
@@ -46,10 +46,31 @@ func TestRoleStoreCache(t *testing.T) {
cachedStore, err := NewLocalCacheLayer(mockStore, nil, nil, mockCacheProvider, logger)
require.NoError(t, err)
cachedStore.Role().GetByName(context.Background(), "role-name")
_, err = cachedStore.Role().GetByName(context.Background(), "role-name")
require.NoError(t, err)
mockStore.Role().(*mocks.RoleStore).AssertNumberOfCalls(t, "GetByName", 1)
cachedStore.Role().Save(&fakeRole)
cachedStore.Role().GetByName(context.Background(), "role-name")
_, err = cachedStore.Role().Save(&fakeRole)
require.NoError(t, err)
mockStore.Role().(*mocks.RoleStore).AssertNumberOfCalls(t, "Save", 1)
_, err = cachedStore.Role().GetByName(context.Background(), "role-name")
require.NoError(t, err)
mockStore.Role().(*mocks.RoleStore).AssertNumberOfCalls(t, "GetByName", 2)
})
t.Run("first call not cached, save preserving unknown permissions, and then not cached again", func(t *testing.T) {
mockStore := getMockStore(t)
mockCacheProvider := getMockCacheProvider()
cachedStore, err := NewLocalCacheLayer(mockStore, nil, nil, mockCacheProvider, logger)
require.NoError(t, err)
_, err = cachedStore.Role().GetByName(context.Background(), "role-name")
require.NoError(t, err)
mockStore.Role().(*mocks.RoleStore).AssertNumberOfCalls(t, "GetByName", 1)
_, err = cachedStore.Role().SavePreservingUnknownPermissions(&fakeRole)
require.NoError(t, err)
mockStore.Role().(*mocks.RoleStore).AssertNumberOfCalls(t, "SavePreservingUnknownPermissions", 1)
_, err = cachedStore.Role().GetByName(context.Background(), "role-name")
require.NoError(t, err)
mockStore.Role().(*mocks.RoleStore).AssertNumberOfCalls(t, "GetByName", 2)
})
@@ -12384,6 +12384,27 @@ func (s *RetryLayerRoleStore) Save(role *model.Role) (*model.Role, error) {
}
func (s *RetryLayerRoleStore) SavePreservingUnknownPermissions(role *model.Role) (*model.Role, error) {
tries := 0
for {
result, err := s.RoleStore.SavePreservingUnknownPermissions(role)
if err == nil {
return result, nil
}
if !isRepeatableError(err) {
return result, err
}
tries++
if tries >= 3 {
err = errors.Wrap(err, "giving up after 3 consecutive repeatable transaction failures")
return result, err
}
timepkg.Sleep(100 * timepkg.Millisecond)
}
}
func (s *RetryLayerScheduledPostStore) CreateScheduledPost(scheduledPost *model.ScheduledPost) (*model.ScheduledPost, error) {
tries := 0
+53 -3
View File
@@ -13,6 +13,7 @@ import (
"github.com/pkg/errors"
"github.com/mattermost/mattermost/server/public/model"
"github.com/mattermost/mattermost/server/public/shared/mlog"
"github.com/mattermost/mattermost/server/v8/channels/store"
)
@@ -99,10 +100,59 @@ func newSqlRoleStore(sqlStore *SqlStore) store.RoleStore {
return &s
}
func (s *SqlRoleStore) Save(role *model.Role) (_ *model.Role, err error) {
func (s *SqlRoleStore) Save(role *model.Role) (*model.Role, error) {
return s.save(role, false)
}
// SavePreservingUnknownPermissions behaves like Save but tolerates and persists
// permissions this server build does not recognize. See the RoleStore interface
// and MM-68830 for the downgrade scenario this protects against.
func (s *SqlRoleStore) SavePreservingUnknownPermissions(role *model.Role) (*model.Role, error) {
return s.save(role, true)
}
// validateForSave validates the role before it is persisted. When
// preserveUnknownPermissions is true, permissions unknown to this server build are
// logged and excluded from the validation (but left untouched on the role so they
// are still persisted), rather than causing the save to fail.
func (s *SqlRoleStore) validateForSave(role *model.Role, preserveUnknownPermissions bool) error {
roleToValidate := role
if preserveUnknownPermissions {
if unknown := role.UnknownPermissions(); len(unknown) > 0 {
s.Logger().Warn(
"Preserving role permissions not recognized by this server version (server likely downgraded from a newer release)",
mlog.String("role", role.Name),
mlog.Array("permissions", unknown),
)
unknownSet := make(map[string]bool, len(unknown))
for _, permission := range unknown {
unknownSet[permission] = true
}
known := make([]string, 0, len(role.Permissions))
for _, permission := range role.Permissions {
if !unknownSet[permission] {
known = append(known, permission)
}
}
roleCopy := role.Clone()
roleCopy.Permissions = known
roleToValidate = roleCopy
}
}
if err := roleToValidate.IsValidWithoutId(); err != nil {
return store.NewErrInvalidInput("Role", "<any>", err.Error())
}
return nil
}
func (s *SqlRoleStore) save(role *model.Role, preserveUnknownPermissions bool) (*model.Role, error) {
// Check the role is valid before proceeding.
if err = role.IsValidWithoutId(); err != nil {
return nil, store.NewErrInvalidInput("Role", "<any>", err.Error())
if err := s.validateForSave(role, preserveUnknownPermissions); err != nil {
return nil, err
}
if role.Id == "" {
@@ -6,9 +6,43 @@ package sqlstore
import (
"testing"
"github.com/stretchr/testify/require"
"github.com/mattermost/mattermost/server/public/model"
"github.com/mattermost/mattermost/server/public/shared/request"
"github.com/mattermost/mattermost/server/v8/channels/store"
"github.com/mattermost/mattermost/server/v8/channels/store/storetest"
)
func TestRoleStore(t *testing.T) {
StoreTestWithSqlStore(t, storetest.TestRoleStore)
}
// TestSqlRoleStoreCreateRoleValidates guards a regression: createRole must validate
// the role itself. It is called directly by scheme_store (bypassing Save/save and
// their validation), so removing its own validation would silently let invalid roles
// through that path (see MM-68830 review).
func TestSqlRoleStoreCreateRoleValidates(t *testing.T) {
StoreTestWithSqlStore(t, func(t *testing.T, rctx request.CTX, ss store.Store, s storetest.SqlStore) {
roleStore := ss.Role().(*SqlRoleStore)
transaction, err := roleStore.GetMaster().Begin()
require.NoError(t, err)
defer func() { _ = transaction.Rollback() }()
// A role carrying a permission this build does not recognize must be rejected
// by createRole, just as it is by Save. createRole does not tolerate unknown
// permissions; only the migration's SavePreservingUnknownPermissions path does.
invalid := &model.Role{
Name: model.NewId(),
DisplayName: model.NewId(),
Description: model.NewId(),
Permissions: []string{"manage_own_agent_from_the_future"},
}
_, err = roleStore.createRole(invalid, transaction)
require.Error(t, err, "createRole must reject unknown permissions")
var invErr *store.ErrInvalidInput
require.ErrorAs(t, err, &invErr)
})
}
+4
View File
@@ -870,6 +870,10 @@ type PluginStore interface {
type RoleStore interface {
Save(role *model.Role) (*model.Role, error)
// SavePreservingUnknownPermissions behaves like Save but tolerates and preserves
// permissions not recognized by this server build instead of rejecting the role.
// Unrecognized permissions are logged (see MM-68830).
SavePreservingUnknownPermissions(role *model.Role) (*model.Role, error)
Get(roleID string) (*model.Role, error)
GetAll() ([]*model.Role, error)
GetByName(ctx context.Context, name string) (*model.Role, error)
@@ -304,6 +304,36 @@ func (_m *RoleStore) Save(role *model.Role) (*model.Role, error) {
return r0, r1
}
// SavePreservingUnknownPermissions provides a mock function with given fields: role
func (_m *RoleStore) SavePreservingUnknownPermissions(role *model.Role) (*model.Role, error) {
ret := _m.Called(role)
if len(ret) == 0 {
panic("no return value specified for SavePreservingUnknownPermissions")
}
var r0 *model.Role
var r1 error
if rf, ok := ret.Get(0).(func(*model.Role) (*model.Role, error)); ok {
return rf(role)
}
if rf, ok := ret.Get(0).(func(*model.Role) *model.Role); ok {
r0 = rf(role)
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).(*model.Role)
}
}
if rf, ok := ret.Get(1).(func(*model.Role) error); ok {
r1 = rf(role)
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// NewRoleStore creates a new instance of RoleStore. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations.
// The first argument is typically a *testing.T value.
func NewRoleStore(t interface {
@@ -18,6 +18,7 @@ import (
func TestRoleStore(t *testing.T, rctx request.CTX, ss store.Store, s SqlStore) {
t.Run("Save", func(t *testing.T) { testRoleStoreSave(t, rctx, ss) })
t.Run("SavePreservingUnknownPermissions", func(t *testing.T) { testRoleStoreSavePreservingUnknownPermissions(t, rctx, ss) })
t.Run("Get", func(t *testing.T) { testRoleStoreGet(t, rctx, ss) })
t.Run("GetAll", func(t *testing.T) { testRoleStoreGetAll(t, rctx, ss) })
t.Run("GetByName", func(t *testing.T) { testRoleStoreGetByName(t, rctx, ss) })
@@ -103,6 +104,73 @@ func testRoleStoreSave(t *testing.T, rctx request.CTX, ss store.Store) {
assert.Error(t, err)
}
func testRoleStoreSavePreservingUnknownPermissions(t *testing.T, _ request.CTX, ss store.Store) {
// A role whose permissions are all valid for this build saves and round-trips
// unchanged, just like Save.
t.Run("preserves known permissions like Save", func(t *testing.T) {
r := &model.Role{
Name: model.NewId(),
DisplayName: model.NewId(),
Description: model.NewId(),
Permissions: []string{"invite_user", "add_user_to_team"},
SchemeManaged: false,
}
saved, err := ss.Role().SavePreservingUnknownPermissions(r)
require.NoError(t, err)
assert.Equal(t, r.Permissions, saved.Permissions)
})
// The downgrade scenario from MM-68830: an existing role gains a permission this
// build does not recognize (written by a newer release before the downgrade). The
// migration re-saves every role; Save would reject the unknown permission, but
// SavePreservingUnknownPermissions must keep it so it is not lost on a future upgrade.
t.Run("tolerates and persists unknown permissions on update", func(t *testing.T) {
unknown := "manage_own_agent_from_the_future"
// Create the role as a known-good role first (the pre-downgrade state).
existing, err := ss.Role().Save(&model.Role{
Name: model.NewId(),
DisplayName: model.NewId(),
Description: model.NewId(),
Permissions: []string{"invite_user"},
})
require.NoError(t, err)
// Simulate the newer release having added an unknown permission to the role.
existing.Permissions = append(existing.Permissions, unknown)
// Sanity check: the regular Save rejects the unknown permission.
_, err = ss.Role().Save(existing)
require.Error(t, err)
saved, err := ss.Role().SavePreservingUnknownPermissions(existing)
require.NoError(t, err)
assert.Contains(t, saved.Permissions, unknown, "unknown permission should be preserved")
// It must actually be persisted, not just returned.
fetched, err := ss.Role().Get(saved.Id)
require.NoError(t, err)
assert.Contains(t, fetched.Permissions, unknown)
})
// Tolerating unknown permissions must not mask genuine structural problems.
t.Run("still rejects structurally invalid roles", func(t *testing.T) {
r := &model.Role{
Name: "invalid-name",
DisplayName: model.NewId(),
Description: model.NewId(),
Permissions: []string{"manage_own_agent_from_the_future"},
SchemeManaged: false,
}
_, err := ss.Role().SavePreservingUnknownPermissions(r)
require.Error(t, err)
var invErr *store.ErrInvalidInput
require.ErrorAs(t, err, &invErr)
})
}
func testRoleStoreGetAll(t *testing.T, rctx request.CTX, ss store.Store) {
prev, err := ss.Role().GetAll()
require.NoError(t, err)
@@ -9817,6 +9817,22 @@ func (s *TimerLayerRoleStore) Save(role *model.Role) (*model.Role, error) {
return result, err
}
func (s *TimerLayerRoleStore) SavePreservingUnknownPermissions(role *model.Role) (*model.Role, error) {
start := time.Now()
result, err := s.RoleStore.SavePreservingUnknownPermissions(role)
elapsed := float64(time.Since(start)) / float64(time.Second)
if s.Root.Metrics != nil {
success := "false"
if err == nil {
success = "true"
}
s.Root.Metrics.ObserveStoreMethodDuration("RoleStore.SavePreservingUnknownPermissions", success, elapsed)
}
return result, err
}
func (s *TimerLayerScheduledPostStore) CreateScheduledPost(scheduledPost *model.ScheduledPost) (*model.ScheduledPost, error) {
start := time.Now()
+27 -3
View File
@@ -428,6 +428,19 @@ type Role struct {
SchemeId *string `json:"scheme_id"`
}
func (r *Role) Clone() *Role {
rCopy := *r
if r.Permissions != nil {
rCopy.Permissions = make([]string, len(r.Permissions))
copy(rCopy.Permissions, r.Permissions)
}
if r.SchemeId != nil {
schemeId := *r.SchemeId
rCopy.SchemeId = &schemeId
}
return &rCopy
}
func (r *Role) Auditable() map[string]any {
return map[string]any{
"id": r.Id,
@@ -802,6 +815,16 @@ func (r *Role) IsValidWithoutId() error {
return fmt.Errorf("role description exceeds maximum length of %d", RoleDescriptionMaxLength)
}
if unknown := r.UnknownPermissions(); len(unknown) > 0 {
return fmt.Errorf("unknown permissions: %s", strings.Join(unknown, ", "))
}
return nil
}
// UnknownPermissions returns the permissions on the role that are not present in
// AllPermissions or DeprecatedPermissions (see MM-68830).
func (r *Role) UnknownPermissions() []string {
check := func(perms []*Permission, permission string) bool {
for _, p := range perms {
if permission == p.Id {
@@ -810,13 +833,14 @@ func (r *Role) IsValidWithoutId() error {
}
return false
}
var unknown []string
for _, permission := range r.Permissions {
if !check(AllPermissions, permission) && !check(DeprecatedPermissions, permission) {
return fmt.Errorf("unknown permission %q", permission)
unknown = append(unknown, permission)
}
}
return nil
return unknown
}
func CleanRoleNames(roleNames []string) ([]string, bool) {
+73
View File
@@ -430,6 +430,79 @@ func TestRoleIsValidWithoutId(t *testing.T) {
})
}
func TestRoleUnknownPermissions(t *testing.T) {
t.Run("returns nil when all permissions are known", func(t *testing.T) {
r := &Role{
Permissions: []string{PermissionCreatePost.Id, PermissionCreateEmojis.Id},
}
assert.Empty(t, r.UnknownPermissions())
})
t.Run("tolerates deprecated permissions", func(t *testing.T) {
require.NotEmpty(t, DeprecatedPermissions)
r := &Role{
Permissions: []string{PermissionCreatePost.Id, DeprecatedPermissions[0].Id},
}
assert.Empty(t, r.UnknownPermissions())
})
t.Run("returns only the permissions this build does not recognize", func(t *testing.T) {
r := &Role{
Permissions: []string{PermissionCreatePost.Id, "manage_own_agent_from_the_future", "another_unknown"},
}
assert.ElementsMatch(t, []string{"manage_own_agent_from_the_future", "another_unknown"}, r.UnknownPermissions())
})
t.Run("empty permissions yields no unknowns", func(t *testing.T) {
assert.Empty(t, (&Role{}).UnknownPermissions())
})
}
func TestRoleClone(t *testing.T) {
schemeId := NewId()
original := &Role{
Id: NewId(),
Name: "test_role",
DisplayName: "Test Role",
Description: "desc",
CreateAt: 1000,
UpdateAt: 2000,
DeleteAt: 0,
Permissions: []string{"invite_user", "add_user_to_team"},
SchemeManaged: true,
BuiltIn: false,
SchemeId: &schemeId,
}
t.Run("clone equals original", func(t *testing.T) {
cloned := original.Clone()
assert.Equal(t, original, cloned)
})
t.Run("permissions are deep copied", func(t *testing.T) {
cloned := original.Clone()
cloned.Permissions[0] = "mutated"
assert.Equal(t, "invite_user", original.Permissions[0])
})
t.Run("scheme id pointer is deep copied", func(t *testing.T) {
cloned := original.Clone()
require.NotSame(t, original.SchemeId, cloned.SchemeId)
*cloned.SchemeId = NewId()
assert.Equal(t, schemeId, *original.SchemeId)
})
t.Run("nil permissions stays nil", func(t *testing.T) {
r := &Role{}
assert.Nil(t, r.Clone().Permissions)
})
t.Run("nil scheme id stays nil", func(t *testing.T) {
r := &Role{}
assert.Nil(t, r.Clone().SchemeId)
})
}
func TestRoleIsValid(t *testing.T) {
validRole := func() *Role {
return &Role{