fix: add support for merging 2 roles
This commit is contained in:
parent
09161ff072
commit
328d914071
3 changed files with 229 additions and 1 deletions
|
|
@ -3300,6 +3300,7 @@ class MasterView(View): # pylint: disable=too-many-public-methods
|
||||||
removing_data,
|
removing_data,
|
||||||
keeping_data,
|
keeping_data,
|
||||||
self.merge_get_final_data(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}
|
context = {"removing": removing, "keeping": keeping, "diff": diff}
|
||||||
|
|
|
||||||
|
|
@ -2,7 +2,7 @@
|
||||||
################################################################################
|
################################################################################
|
||||||
#
|
#
|
||||||
# wuttaweb -- Web App for Wutta Framework
|
# wuttaweb -- Web App for Wutta Framework
|
||||||
# Copyright © 2024-2025 Lance Edgar
|
# Copyright © 2024-2026 Lance Edgar
|
||||||
#
|
#
|
||||||
# This file is part of Wutta Framework.
|
# This file is part of Wutta Framework.
|
||||||
#
|
#
|
||||||
|
|
@ -59,6 +59,9 @@ class RoleView(MasterView): # pylint: disable=abstract-method
|
||||||
}
|
}
|
||||||
sort_defaults = "name"
|
sort_defaults = "name"
|
||||||
|
|
||||||
|
mergeable = True
|
||||||
|
merge_additive_fields = ["permission_count", "user_count"]
|
||||||
|
|
||||||
wutta_permissions = None
|
wutta_permissions = None
|
||||||
|
|
||||||
# TODO: master should handle this, possibly via configure_form()
|
# TODO: master should handle this, possibly via configure_form()
|
||||||
|
|
@ -284,6 +287,83 @@ class RoleView(MasterView): # pylint: disable=abstract-method
|
||||||
else:
|
else:
|
||||||
auth.revoke_permission(role, pkey)
|
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
|
@classmethod
|
||||||
def defaults(cls, config): # pylint: disable=empty-docstring
|
def defaults(cls, config): # pylint: disable=empty-docstring
|
||||||
""" """
|
""" """
|
||||||
|
|
|
||||||
|
|
@ -284,6 +284,153 @@ class TestRoleView(WebTestCase):
|
||||||
self.assertIs(role, blokes)
|
self.assertIs(role, blokes)
|
||||||
self.assertEqual(blokes.permissions, ["widgets.polish", "widgets.view"])
|
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):
|
class TestPermissionView(WebTestCase):
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue