3
0
Fork 0

fix: add support for merging 2 roles

This commit is contained in:
Lance Edgar 2026-03-22 11:10:01 -05:00
parent 09161ff072
commit 328d914071
3 changed files with 229 additions and 1 deletions

View file

@ -3300,6 +3300,7 @@ class MasterView(View): # pylint: disable=too-many-public-methods
removing_data,
keeping_data,
self.merge_get_final_data(removing_data, keeping_data),
fields=self.merge_get_all_fields(),
)
context = {"removing": removing, "keeping": keeping, "diff": diff}

View file

@ -2,7 +2,7 @@
################################################################################
#
# wuttaweb -- Web App for Wutta Framework
# Copyright © 2024-2025 Lance Edgar
# Copyright © 2024-2026 Lance Edgar
#
# This file is part of Wutta Framework.
#
@ -59,6 +59,9 @@ class RoleView(MasterView): # pylint: disable=abstract-method
}
sort_defaults = "name"
mergeable = True
merge_additive_fields = ["permission_count", "user_count"]
wutta_permissions = None
# TODO: master should handle this, possibly via configure_form()
@ -284,6 +287,83 @@ class RoleView(MasterView): # pylint: disable=abstract-method
else:
auth.revoke_permission(role, pkey)
def merge_get_data(self, obj): # pylint: disable=empty-docstring
""" """
data = super().merge_get_data(obj)
role = obj
data["permissions"] = role.permissions
data["permission_count"] = len(data["permissions"])
data["usernames"] = [user.username for user in role.users]
data["user_count"] = len(data["usernames"])
return data
def merge_get_final_data(
self, removing, keeping
): # pylint: disable=empty-docstring
""" """
final = super().merge_get_final_data(removing, keeping)
permissions = set(removing["permissions"] + keeping["permissions"])
final["permission_count"] = len(permissions)
usernames = set(removing["usernames"] + keeping["usernames"])
final["user_count"] = len(usernames)
return final
def merge_why_not(self, removing, keeping):
"""
This checks to ensure the "removing" role is not one of the
special built-in roles (Administrator, Authenticated,
Anonymous).
See also parent method:
:meth:`~wuttaweb.views.master.MasterView.merge_why_not()`
"""
auth = self.app.get_auth_handler()
session = self.Session()
if removing is auth.get_role_administrator(session):
return "Cannot remove the Administrator role."
if removing is auth.get_role_anonymous(session):
return "Cannot remove the Anonymous role."
if removing is auth.get_role_authenticated(session):
return "Cannot remove the Authenticated role."
return None
def merge_execute(self, removing, keeping):
"""
The logic to merge 2 roles is extended as follows:
Any users belonging to the "removing" role will be added to
the "keeping" role (if not already present).
Any permissions belonging to the "removing" role will be added
to the "keeping" role (if not already present).
See also parent method:
:meth:`~wuttaweb.views.master.MasterView.merge_execute()`
"""
# transfer permissions
for perm in list(removing.permissions):
if perm not in keeping.permissions:
keeping.permissions.append(perm)
# transfer users
for user in list(removing.users):
if user not in keeping.users:
keeping.users.append(user)
# continue default merge
super().merge_execute(removing, keeping)
@classmethod
def defaults(cls, config): # pylint: disable=empty-docstring
""" """

View file

@ -284,6 +284,153 @@ class TestRoleView(WebTestCase):
self.assertIs(role, blokes)
self.assertEqual(blokes.permissions, ["widgets.polish", "widgets.view"])
def test_merge_get_data(self):
model = self.app.model
auth = self.app.get_auth_handler()
role = model.Role(name="whatever")
auth.grant_permission(role, "people.list")
auth.grant_permission(role, "people.view")
auth.grant_permission(role, "people.edit")
self.session.add(role)
user1 = model.User(username="user1")
user1.roles.append(role)
self.session.add(user1)
user2 = model.User(username="user2")
user2.roles.append(role)
self.session.add(user2)
self.session.commit()
self.assertEqual(len(role.permissions), 3)
self.assertEqual(len(role.users), 2)
view = self.make_view()
with patch.object(view, "Session", return_value=self.session):
data = view.merge_get_data(role)
self.assertEqual(
sorted(data["permissions"]),
["people.edit", "people.list", "people.view"],
)
self.assertEqual(data["permission_count"], 3)
self.assertEqual(data["usernames"], ["user1", "user2"])
self.assertEqual(data["user_count"], 2)
def test_merge_get_final_data(self):
model = self.app.model
auth = self.app.get_auth_handler()
role1 = model.Role(name="whatever1")
auth.grant_permission(role1, "people.list")
auth.grant_permission(role1, "people.view")
auth.grant_permission(role1, "people.edit")
self.session.add(role1)
role2 = model.Role(name="whatever2")
auth.grant_permission(role2, "people.list")
auth.grant_permission(role2, "people.view")
self.session.add(role2)
user1 = model.User(username="user1")
user1.roles.append(role1)
self.session.add(user1)
user2 = model.User(username="user2")
user2.roles.append(role1)
user2.roles.append(role2)
self.session.add(user2)
self.session.commit()
self.assertEqual(len(role1.permissions), 3)
self.assertEqual(len(role1.users), 2)
self.assertEqual(len(role2.permissions), 2)
self.assertEqual(len(role2.users), 1)
view = self.make_view()
with patch.object(view, "Session", return_value=self.session):
removing = view.merge_get_data(role1)
keeping = view.merge_get_data(role2)
final = view.merge_get_final_data(removing, keeping)
self.assertEqual(final["permission_count"], 3)
self.assertEqual(final["user_count"], 2)
def test_merge_why_not(self):
model = self.app.model
auth = self.app.get_auth_handler()
role1 = model.Role(name="whatever1")
self.session.add(role1)
role2 = model.Role(name="whatever2")
self.session.add(role2)
self.session.commit()
view = self.make_view()
with patch.object(view, "Session", return_value=self.session):
# normal merge is allowed
self.assertIsNone(view.merge_why_not(role1, role2))
# special roles can be part of a merge if they are being "kept"
# but not if being "removed"
admin = auth.get_role_administrator(self.session)
self.assertIsNone(view.merge_why_not(role1, admin))
reason = view.merge_why_not(admin, role1)
self.assertEqual(reason, "Cannot remove the Administrator role.")
authed = auth.get_role_authenticated(self.session)
self.assertIsNone(view.merge_why_not(role1, authed))
reason = view.merge_why_not(authed, role1)
self.assertEqual(reason, "Cannot remove the Authenticated role.")
anon = auth.get_role_anonymous(self.session)
self.assertIsNone(view.merge_why_not(role1, anon))
reason = view.merge_why_not(anon, role1)
self.assertEqual(reason, "Cannot remove the Anonymous role.")
def test_merge_execute(self):
model = self.app.model
auth = self.app.get_auth_handler()
role1 = model.Role(name="whatever1")
auth.grant_permission(role1, "people.list")
auth.grant_permission(role1, "people.view")
auth.grant_permission(role1, "people.edit")
self.session.add(role1)
role2 = model.Role(name="whatever2")
auth.grant_permission(role2, "people.list")
auth.grant_permission(role2, "people.view")
self.session.add(role2)
user1 = model.User(username="user1")
user1.roles.append(role1)
self.session.add(user1)
user2 = model.User(username="user2")
user2.roles.append(role1)
user2.roles.append(role2)
self.session.add(user2)
self.session.commit()
self.assertEqual(self.session.query(model.Role).count(), 2)
self.assertEqual(len(role1.permissions), 3)
self.assertEqual(len(role1.users), 2)
self.assertEqual(len(role2.permissions), 2)
self.assertEqual(len(role2.users), 1)
view = self.make_view()
with patch.object(view, "Session", return_value=self.session):
view.merge_execute(role1, role2)
self.session.commit()
self.assertEqual(self.session.query(model.Role).count(), 1)
self.assertNotIn(role1, self.session)
self.assertIn(role2, self.session)
self.assertIs(role2, self.session.query(model.Role).one())
self.assertEqual(len(role2.permissions), 3)
self.assertEqual(len(role2.users), 2)
class TestPermissionView(WebTestCase):