From a0907ec94df45bd0dec5ee5becad3f50047c9abf Mon Sep 17 00:00:00 2001 From: Andrew Bastien Date: Sat, 25 Nov 2023 18:09:30 -0500 Subject: [PATCH 1/3] Improved lock-setting logic to handle empty locks, and allow customizing locks via .create()'s new generate_default_locks --- evennia/accounts/accounts.py | 13 ++-- evennia/locks/lockhandler.py | 11 +++- evennia/objects/objects.py | 124 ++++++++++++++++++++++++++++------- 3 files changed, 118 insertions(+), 30 deletions(-) diff --git a/evennia/accounts/accounts.py b/evennia/accounts/accounts.py index 1acf45a2f3..8098724347 100644 --- a/evennia/accounts/accounts.py +++ b/evennia/accounts/accounts.py @@ -277,6 +277,12 @@ class DefaultAccount(AccountDB, metaclass=TypeclassBase): # Used by account.create_character() to choose default typeclass for characters. default_character_typeclass = settings.BASE_CHARACTER_TYPECLASS + lockstring = ( + "examine:perm(Admin);edit:perm(Admin);" + "delete:perm(Admin);boot:perm(Admin);msg:all();" + "noidletimeout:perm(Builder) or perm(noidletimeout)" + ) + # properties @lazy_property def cmdset(self): @@ -1399,12 +1405,7 @@ class DefaultAccount(AccountDB, metaclass=TypeclassBase): """ # A basic security setup - lockstring = ( - "examine:perm(Admin);edit:perm(Admin);" - "delete:perm(Admin);boot:perm(Admin);msg:all();" - "noidletimeout:perm(Builder) or perm(noidletimeout)" - ) - self.locks.add(lockstring) + self.locks.add(self.lockstring) # The ooc account cmdset self.cmdset.add_default(_CMDSET_ACCOUNT, persistent=True) diff --git a/evennia/locks/lockhandler.py b/evennia/locks/lockhandler.py index 29cce23cde..eba0e64509 100644 --- a/evennia/locks/lockhandler.py +++ b/evennia/locks/lockhandler.py @@ -338,9 +338,16 @@ class LockHandler: """ if isinstance(lockstring, str): - lockdefs = lockstring.split(";") + lockdefs = [ + stripped for lockdef in lockstring.split(";") if (stripped := lockdef.strip()) + ] else: - lockdefs = [lockdef for locks in lockstring for lockdef in locks.split(";")] + lockdefs = [ + stripped + for locks in lockstring + for lockdef in locks.split(";") + if (stripped := lockdef.strip()) + ] lockstring = ";".join(lockdefs) err = "" diff --git a/evennia/objects/objects.py b/evennia/objects/objects.py index 67ee70b9c8..baec752198 100644 --- a/evennia/objects/objects.py +++ b/evennia/objects/objects.py @@ -1010,6 +1010,26 @@ class DefaultObject(ObjectDB, metaclass=TypeclassBase): obj.msg(_(string)) obj.move_to(home, move_type="teleport") + @classmethod + def generate_default_locks( + cls, account: "DefaultAccount" = None, caller: "DefaultObject" = None, **kwargs + ): + """ + Classmethod called during .create() to determine default locks for the object. + + Args: + account (Account): Account to attribute this object to. + caller (DefaultObject): The object which is creating this one. + **kwargs: Arbitrary input. + + Returns: + lockstring (str): A lockstring to use for this object. + """ + if cls.lockstring: + account_id = account.id if account else -1 + return cls.lockstring.format(account_id=account_id) + return "" + @classmethod def create( cls, @@ -1058,8 +1078,8 @@ class DefaultObject(ObjectDB, metaclass=TypeclassBase): # Create a sane lockstring if one wasn't supplied lockstring = kwargs.get("locks") - if account and not lockstring: - lockstring = cls.lockstring.format(account_id=account.id) + if (account or caller) and not lockstring: + lockstring = cls.generate_default_locks(account=account, caller=caller, **kwargs) kwargs["locks"] = lockstring # Create object @@ -1078,7 +1098,7 @@ class DefaultObject(ObjectDB, metaclass=TypeclassBase): obj.db.desc = desc except Exception as e: - errors.append("An error occurred while creating this '%s' object." % key) + errors.append(f"An error occurred while creating this '{key}' object: {e}") logger.log_err(e) return obj, errors @@ -2503,6 +2523,28 @@ class DefaultCharacter(DefaultObject): "edit:pid({account_id}) or perm(Admin)" ) + @classmethod + def generate_default_locks( + cls, account: "DefaultAccount" = None, caller: "DefaultObject" = None, **kwargs + ): + """ + Classmethod called during .create() to determine default locks for the object. + + Args: + account (Account): Account to attribute this object to. + caller (DefaultObject): The object which is creating this one. + **kwargs: Arbitrary input. + + Returns: + lockstring (str): A lockstring to use for this object. + """ + if cls.lockstring: + account_id = account.id if account else -1 + character = kwargs.get("character", None) + character_id = character.id if character else -1 + return cls.lockstring.format(character_id=character.id, account_id=account_id) + return "" + @classmethod def create(cls, key, account=None, **kwargs): """ @@ -2573,21 +2615,20 @@ class DefaultCharacter(DefaultObject): account.characters.add(obj) # Add locks - if not locks and account: + if not locks: # Allow only the character itself and the creator account to puppet this character # (and Developers). - locks = cls.lockstring.format(character_id=obj.id, account_id=account.id) - elif not locks and not account: - locks = cls.lockstring.format(character_id=obj.id, account_id=-1) + locks = cls.generate_default_locks(account=account, character=obj) - obj.locks.add(locks) + if locks: + obj.locks.add(locks) # If no description is set, set a default description if description or not obj.db.desc: obj.db.desc = description if description else _("This is a character.") except Exception as e: - errors.append(f"An error occurred while creating object '{key} object.") + errors.append(f"An error occurred while creating object '{key} object: {e}") logger.log_err(e) return obj, errors @@ -2793,6 +2834,27 @@ class DefaultRoom(DefaultObject): "edit:id({id}) or perm(Admin)" ) + @classmethod + def generate_default_locks( + cls, account: "DefaultAccount" = None, caller: "DefaultObject" = None, **kwargs + ): + """ + Classmethod called during .create() to determine default locks for the object. + + Args: + account (Account): Account to attribute this object to. + caller (DefaultObject): The object which is creating this one. + **kwargs: Arbitrary input. + + Returns: + lockstring (str): A lockstring to use for this object. + """ + if cls.lockstring: + room = kwargs.get("room") + id = account.id if account else caller.id if caller else room.id + return cls.lockstring.format(id=id) + return "" + @classmethod def create( cls, @@ -2851,12 +2913,10 @@ class DefaultRoom(DefaultObject): obj = create.create_object(**kwargs) # Add locks - if not locks and account: - locks = cls.lockstring.format(id=account.id) - elif not locks and not account: - locks = cls.lockstring.format(id=obj.id) - - obj.locks.add(locks) + if not locks: + locks = cls.generate_default_locks(account=account, caller=caller, room=obj) + if locks: + obj.locks.add(locks) # Record creator id and creation IP if ip: @@ -2869,7 +2929,7 @@ class DefaultRoom(DefaultObject): obj.db.desc = description if description else _("This is a room.") except Exception as e: - errors.append("An error occurred while creating this '%s' object." % key) + errors.append(f"An error occurred while creating this '{key}' object: {e}") logger.log_err(e) return obj, errors @@ -3008,6 +3068,27 @@ class DefaultExit(DefaultObject): # Command hooks + @classmethod + def generate_default_locks( + cls, account: "DefaultAccount" = None, caller: "DefaultObject" = None, **kwargs + ): + """ + Classmethod called during .create() to determine default locks for the object. + + Args: + account (Account): Account to attribute this object to. + caller (DefaultObject): The object which is creating this one. + **kwargs: Arbitrary input. + + Returns: + lockstring (str): A lockstring to use for this object. + """ + if cls.lockstring: + room = kwargs.get("room", None) + id = account.id if account else caller.id if caller else room.id if room else -1 + return cls.lockstring.format(id=id) + return "" + @classmethod def create( cls, @@ -3070,11 +3151,10 @@ class DefaultExit(DefaultObject): obj = create.create_object(**kwargs) # Set appropriate locks - if not locks and account: - locks = cls.lockstring.format(id=account.id) - elif not locks and not account: - locks = cls.lockstring.format(id=obj.id) - obj.locks.add(locks) + if not locks: + locks = cls.generate_default_locks(account=account, caller=caller, exit=obj) + if locks: + obj.locks.add(locks) # Record creator id and creation IP if ip: @@ -3087,7 +3167,7 @@ class DefaultExit(DefaultObject): obj.db.desc = description if description else _("This is an exit.") except Exception as e: - errors.append("An error occurred while creating this '%s' object." % key) + errors.append(f"An error occurred while creating this '{key}' object: {e}") logger.log_err(e) return obj, errors From 7685ce96239f779f383b961d252a77aedb9f56f9 Mon Sep 17 00:00:00 2001 From: Andrew Bastien Date: Sat, 2 Dec 2023 20:21:37 -0500 Subject: [PATCH 2/3] Renaming things and cleaning them up a bit. --- evennia/objects/objects.py | 100 ++++++++----------------------------- 1 file changed, 22 insertions(+), 78 deletions(-) diff --git a/evennia/objects/objects.py b/evennia/objects/objects.py index baec752198..31fac19381 100644 --- a/evennia/objects/objects.py +++ b/evennia/objects/objects.py @@ -207,10 +207,6 @@ class DefaultObject(ObjectDB, metaclass=TypeclassBase): # Used for sorting / filtering in inventories / room contents. _content_types = ("object",) - # lockstring of newly created objects, for easy overloading. - # Will be formatted with the appropriate attributes. - lockstring = "control:id({account_id}) or perm(Admin);delete:id({account_id}) or perm(Admin)" - objects = ObjectManager() # populated by `return_appearance` @@ -1011,7 +1007,7 @@ class DefaultObject(ObjectDB, metaclass=TypeclassBase): obj.move_to(home, move_type="teleport") @classmethod - def generate_default_locks( + def get_default_lockstring( cls, account: "DefaultAccount" = None, caller: "DefaultObject" = None, **kwargs ): """ @@ -1025,10 +1021,11 @@ class DefaultObject(ObjectDB, metaclass=TypeclassBase): Returns: lockstring (str): A lockstring to use for this object. """ - if cls.lockstring: - account_id = account.id if account else -1 - return cls.lockstring.format(account_id=account_id) - return "" + pid = f"pid({account.id})" if account else None + cid = f"id({caller.id})" if caller else None + admin = "perm(Admin)" + trio = " or ".join([x for x in [pid, cid, admin] if x]) + return ";".join([f"{x}:{trio}" for x in ["control", "delete", "edit"]]) @classmethod def create( @@ -1079,7 +1076,7 @@ class DefaultObject(ObjectDB, metaclass=TypeclassBase): # Create a sane lockstring if one wasn't supplied lockstring = kwargs.get("locks") if (account or caller) and not lockstring: - lockstring = cls.generate_default_locks(account=account, caller=caller, **kwargs) + lockstring = cls.get_default_lockstring(account=account, caller=caller, **kwargs) kwargs["locks"] = lockstring # Create object @@ -2524,7 +2521,7 @@ class DefaultCharacter(DefaultObject): ) @classmethod - def generate_default_locks( + def get_default_lockstring( cls, account: "DefaultAccount" = None, caller: "DefaultObject" = None, **kwargs ): """ @@ -2538,12 +2535,17 @@ class DefaultCharacter(DefaultObject): Returns: lockstring (str): A lockstring to use for this object. """ - if cls.lockstring: - account_id = account.id if account else -1 - character = kwargs.get("character", None) - character_id = character.id if character else -1 - return cls.lockstring.format(character_id=character.id, account_id=account_id) - return "" + pid = f"pid({account.id})" if account else None + character = kwargs.get("character", None) + cid = f"id({character})" if character else None + + puppet = "puppet:" + " or ".join( + [x for x in [pid, cid, "perm(Developer)", "pperm(Developer)"] if x] + ) + delete = "delete:" + " or ".join([x for x in [pid, "perm(Admin)"] if x]) + edit = "edit:" + " or ".join([x for x in [pid, "perm(Admin)"] if x]) + + return ";".join([puppet, delete, edit]) @classmethod def create(cls, key, account=None, **kwargs): @@ -2618,7 +2620,7 @@ class DefaultCharacter(DefaultObject): if not locks: # Allow only the character itself and the creator account to puppet this character # (and Developers). - locks = cls.generate_default_locks(account=account, character=obj) + locks = cls.get_default_lockstring(account=account, character=obj) if locks: obj.locks.add(locks) @@ -2826,35 +2828,6 @@ class DefaultRoom(DefaultObject): # Generally, a room isn't expected to HAVE a location, but maybe in some games? _content_types = ("room",) - # lockstring of newly created rooms, for easy overloading. - # Will be formatted with the {id} of the creating object. - lockstring = ( - "control:id({id}) or perm(Admin); " - "delete:id({id}) or perm(Admin); " - "edit:id({id}) or perm(Admin)" - ) - - @classmethod - def generate_default_locks( - cls, account: "DefaultAccount" = None, caller: "DefaultObject" = None, **kwargs - ): - """ - Classmethod called during .create() to determine default locks for the object. - - Args: - account (Account): Account to attribute this object to. - caller (DefaultObject): The object which is creating this one. - **kwargs: Arbitrary input. - - Returns: - lockstring (str): A lockstring to use for this object. - """ - if cls.lockstring: - room = kwargs.get("room") - id = account.id if account else caller.id if caller else room.id - return cls.lockstring.format(id=id) - return "" - @classmethod def create( cls, @@ -2914,7 +2887,7 @@ class DefaultRoom(DefaultObject): # Add locks if not locks: - locks = cls.generate_default_locks(account=account, caller=caller, room=obj) + locks = cls.get_default_lockstring(account=account, caller=caller, room=obj) if locks: obj.locks.add(locks) @@ -3020,14 +2993,6 @@ class DefaultExit(DefaultObject): exit_command = ExitCommand priority = 101 - # lockstring of newly created exits, for easy overloading. - # Will be formatted with the {id} of the creating object. - lockstring = ( - "control:id({id}) or perm(Admin); " - "delete:id({id}) or perm(Admin); " - "edit:id({id}) or perm(Admin)" - ) - # Helper classes and methods to implement the Exit. These need not # be overloaded unless one want to change the foundation for how # Exits work. See the end of the class for hook methods to overload. @@ -3068,27 +3033,6 @@ class DefaultExit(DefaultObject): # Command hooks - @classmethod - def generate_default_locks( - cls, account: "DefaultAccount" = None, caller: "DefaultObject" = None, **kwargs - ): - """ - Classmethod called during .create() to determine default locks for the object. - - Args: - account (Account): Account to attribute this object to. - caller (DefaultObject): The object which is creating this one. - **kwargs: Arbitrary input. - - Returns: - lockstring (str): A lockstring to use for this object. - """ - if cls.lockstring: - room = kwargs.get("room", None) - id = account.id if account else caller.id if caller else room.id if room else -1 - return cls.lockstring.format(id=id) - return "" - @classmethod def create( cls, @@ -3152,7 +3096,7 @@ class DefaultExit(DefaultObject): # Set appropriate locks if not locks: - locks = cls.generate_default_locks(account=account, caller=caller, exit=obj) + locks = cls.get_default_lockstring(account=account, caller=caller, exit=obj) if locks: obj.locks.add(locks) From b3f4962f7ee0cabbdcbb94a8000885d86789bf30 Mon Sep 17 00:00:00 2001 From: Andrew Bastien Date: Sat, 16 Dec 2023 17:30:44 -0500 Subject: [PATCH 3/3] Added tests for new lock features. --- evennia/objects/tests.py | 25 +++++++++++++++++++++++++ 1 file changed, 25 insertions(+) diff --git a/evennia/objects/tests.py b/evennia/objects/tests.py index ea1ac4bfc3..34f3c589bc 100644 --- a/evennia/objects/tests.py +++ b/evennia/objects/tests.py @@ -114,6 +114,31 @@ class DefaultObjectTest(BaseEvenniaTest): # partial match to 'colon' - multimatch error since stack is not homogenous self.assertEqual(self.char1.search("co", stacked=2), None) + def test_get_default_lockstring_base(self): + pattern = f"control:pid({self.account.id}) or id({self.char1.id}) or perm(Admin);delete:pid({self.account.id}) or id({self.char1.id}) or perm(Admin);edit:pid({self.account.id}) or id({self.char1.id}) or perm(Admin)" + self.assertEqual( + DefaultObject.get_default_lockstring(account=self.account, caller=self.char1), pattern + ) + + def test_get_default_lockstring_room(self): + pattern = f"control:pid({self.account.id}) or id({self.char1.id}) or perm(Admin);delete:pid({self.account.id}) or id({self.char1.id}) or perm(Admin);edit:pid({self.account.id}) or id({self.char1.id}) or perm(Admin)" + self.assertEqual( + DefaultRoom.get_default_lockstring(account=self.account, caller=self.char1), pattern + ) + + def test_get_default_lockstring_exit(self): + pattern = f"control:pid({self.account.id}) or id({self.char1.id}) or perm(Admin);delete:pid({self.account.id}) or id({self.char1.id}) or perm(Admin);edit:pid({self.account.id}) or id({self.char1.id}) or perm(Admin)" + self.assertEqual( + DefaultExit.get_default_lockstring(account=self.account, caller=self.char1), pattern + ) + + def test_get_default_lockstring_character(self): + pattern = f"puppet:pid({self.account.id}) or perm(Developer) or pperm(Developer);delete:pid({self.account.id}) or perm(Admin);edit:pid({self.account.id}) or perm(Admin)" + self.assertEqual( + DefaultCharacter.get_default_lockstring(account=self.account, caller=self.char1), + pattern, + ) + class TestObjectManager(BaseEvenniaTest): "Test object manager methods"