From 66095a0b164de1191642c4c6f8cee46e0cc895e4 Mon Sep 17 00:00:00 2001 From: Griatch Date: Mon, 5 Oct 2009 20:04:15 +0000 Subject: [PATCH] Implemented locks. The main command to use is @lock, which accept three types of locks at the moment, and three types of keys: Locks: DefaultLock, UseLock, EnterLock Keys: ObjectIDs, Groups, Permissions This offers the most useful functionality - stopping people from picking up things, blocking exits and stopping anyone from using an object. If the attributes lock_msg, use_lock_msg and enter_lock_msg are defined on the locked object, these will be used as error messages instead of a standard one (so "the door is locked" instead of "you cannot traverse that exit"). Behind the scenes, there is a new module, src/locks.py that defines Keys and Locks. A Locks object is a collection of Lock types. This is stored in the LOCKS attribute on objects. Each Lock contains a set of Keys that might be of mixed type and which the player must match in order to pass the lock. /Griatch --- src/cmdhandler.py | 25 ++- src/commands/general.py | 21 ++- src/commands/objmanip.py | 153 ++++++++++++++++++- src/defines_global.py | 4 +- src/locks.py | 246 ++++++++++++++++++++++++++++++ src/objects/models.py | 31 +++- src/script_parents/basicobject.py | 37 +++-- 7 files changed, 491 insertions(+), 26 deletions(-) create mode 100644 src/locks.py diff --git a/src/cmdhandler.py b/src/cmdhandler.py index 61f2416460..d461b082b3 100755 --- a/src/cmdhandler.py +++ b/src/cmdhandler.py @@ -278,7 +278,11 @@ def match_exits(command,test=False): if targ_exit.get_home(): # SCRIPT: See if the player can traverse the exit if not targ_exit.scriptlink.default_lock(source_object): - source_object.emit_to("You can't traverse that exit.") + lock_msg = targ_exit.get_attribute_value("lock_msg") + if lock_msg: + source_object.emit_to(lock_msg) + else: + source_object.emit_to("You can't traverse that exit.") else: source_object.move_to(targ_exit.get_home()) else: @@ -287,22 +291,33 @@ def match_exits(command,test=False): raise ExitCommandHandler -def command_table_lookup(command, command_table, eval_perms=True,test=False): +def command_table_lookup(command, command_table, eval_perms=True,test=False,neighbor=None): """ Performs a command table lookup on the specified command table. Also evaluates the permissions tuple. The test flag only checks without manipulating the command + neighbor (object) If this is supplied, we are looking at a object table and + must check for locks. """ # Get the command's function reference (Or False) cmdtuple = command_table.get_command_tuple(command.command_string) if cmdtuple: + # Check if this is just a test. if test: return True + # Check locks + if neighbor and not neighbor.scriptlink.use_lock(command.source_object): + # send an locked error message only if lock_desc is defined + lock_msg = neighbor.get_attribute_value("use_lock_msg") + if lock_msg: + command.source_object.emit_to(lock_msg) + raise ExitCommandHandler + return False # If there is a permissions element to the entry, check perms. if eval_perms and cmdtuple[1]: if not command.source_object.has_perm_list(cmdtuple[1]): command.source_object.emit_to(defines_global.NOPERMS_MSG) - raise ExitCommandHandler + raise ExitCommandHandler # If flow reaches this point, user has perms and command is ready. command.command_function = cmdtuple[0] command.extra_vars = cmdtuple[2] @@ -321,7 +336,9 @@ def match_neighbor_ctables(command,test=False): neighbors = source_object.location.get_contents() for neighbor in neighbors: if command_table_lookup(command, - neighbor.scriptlink.command_table, test=test): + neighbor.scriptlink.command_table, + test=test, neighbor=neighbor): + # test for a use-lock # If there was a command match, set the scripted_obj attribute # for the script parent to pick up. if test: diff --git a/src/commands/general.py b/src/commands/general.py index 63b6b7cf0b..5e0b620d1d 100644 --- a/src/commands/general.py +++ b/src/commands/general.py @@ -167,13 +167,22 @@ def cmd_get(command): return if not obj_is_staff and (target_obj.is_player() or target_obj.is_exit()): + source_object.emit_to("You can't get that.") return - + if target_obj.is_room() or target_obj.is_garbage() or target_obj.is_going(): source_object.emit_to("You can't get that.") return - + + if not target_obj.scriptlink.default_lock(source_object): + lock_msg = target_obj.get_attribute_value("lock_msg") + if lock_msg: + source_object.emit_to(lock_msg) + else: + source_object.emit_to("You can't get that.") + return + target_obj.move_to(source_object, quiet=True) source_object.emit_to("You pick up %s." % (target_obj.get_name(show_dbref=False),)) source_object.get_location().emit_to_contents("%s picks up %s." % @@ -282,11 +291,14 @@ def cmd_examine(command): s += str(target_obj.get_name(fullname=True)) + newl s += str("Type: %s Flags: %s" % (target_obj.get_type(), - target_obj.get_flags())) + newl - #s += str("Desc: %s" % target_obj.get_attribute_value('desc')) + newl + target_obj.get_flags())) + newl s += str("Owner: %s " % target_obj.get_owner()) + newl s += str("Zone: %s" % target_obj.get_zone()) + newl s += str("Parent: %s " % target_obj.get_script_parent()) + newl + + locks = target_obj.get_attribute_value("LOCKS") + if locks and "%s" % locks: + s += str("Locks: %s" % locks) + newl # Contents container lists for sorting by type. con_players = [] @@ -313,6 +325,7 @@ def cmd_examine(command): # This obviously isn't valid for rooms. s += str("Location: %s" % target_obj.get_location()) + newl + # Render other attributes for attribute in target_obj.get_all_attributes(): s += str(attribute.get_attrline()) + newl diff --git a/src/commands/objmanip.py b/src/commands/objmanip.py index 5380f0ecc0..9af69c637e 100644 --- a/src/commands/objmanip.py +++ b/src/commands/objmanip.py @@ -1,9 +1,11 @@ """ These commands typically are to do with building or modifying Objects. """ +from django.contrib.auth.models import Permission, Group from src.objects.models import Object, Attribute # We'll import this as the full path to avoid local variable clashes. import src.flags +from src import locks from src import ansi from src.cmdtable import GLOBAL_CMD_TABLE from src import defines_global @@ -445,7 +447,7 @@ def cmd_create(command): return eq_args = command.command_argument.split(':', 1) - target_name = eq_args[0] + target_name = eq_args[0].strip() #check if we want to set a custom parent script_parent = None @@ -814,7 +816,7 @@ def cmd_dig(command): where you are. Usage: - @dig[/switches] roomname [:parent] [= exitthere [: parent][;alias]] [, exithere [: parent][;alias]] + @dig[/switches] roomname [:parent] [= exit_to_there [: parent][;alias]] [, exit_to_here [: parent][;alias]] switches: teleport - move yourself to the new room @@ -829,7 +831,7 @@ def cmd_dig(command): switches = command.command_switches if not args: - source_object.emit_to("Usage[/teleport]: @dig roomname [:parent][= exitthere [:parent] [;alias]] [, exithere [:parent] [;alias]]") + source_object.emit_to("Usage: @dig[/teleport] roomname [:parent][= exit_to_there [:parent] [;alias]] [, exit_to_here [:parent] [;alias]]") return room_name = None @@ -1158,3 +1160,148 @@ def cmd_destroy(command): GLOBAL_CMD_TABLE.add_command("@destroy", cmd_destroy, priv_tuple=("objects.create",),auto_help=True,staff_help=True) + +def cmd_lock(command): + """@lock + Usage: + @lock[/switch] [:type] [= [,key2,key3,...]] + + switches: + add - add a lock (default) from object + del - remove a lock from object + list - view all locks on object (default) + type: + DefaultLock - the default lock type (default) + + Locks an object for everyone except those matching the keys. + The keys can be of the following types (and searched in this order): + - a user #dbref (#2, #45 etc) + - a Group name (Builder, Immortal etc, case sensitive) + - a Permission string (genperms.get, etc) + If no keys are given, the object is locked for everyone. + + When the lock blocks a user, you may customize which error is given by + storing error messages in an attribute. For DefaultLocks, UseLocks and + EnterLocks, these attributes are called lock_msg, use_lock_msg and + enter_lock_msg respectively. + <> + Lock types: + + Name: Affects: Effect: + ----------------------------------------------------------------------- + DefaultLock: Exits: controls who may traverse the exit to + its destination. + Rooms: controls whether the player sees a failure + message after the room description when + looking at the room. + Players/Things: controls who may 'get' the object. + + UseLock: All but Exits: controls who may use commands defined on + the locked object. + + EnterLock: Players/Things: controls who may enter/teleport into + the object. + + Fail messages echoed to the player are stored in the attributes 'lock_msg', + 'use_lock_msg' and 'enter_lock_msg' on the locked object in question. If no + such message is stored, a default will be used (or none at all in some cases). + """ + + source_object = command.source_object + arg = command.command_argument + switches = command.command_switches + + if not arg: + source_object.emit_to("Usage: @lock[/switch] [:type] [= [,key2,key3,...]]") + return + keys = "" + #deal with all possible arguments. + try: + lside, keys = arg.split("=",1) + except ValueError: + lside = arg + lside, keys = lside.strip(), keys.strip() + try: + obj_name, ltype = lside.split(":",1) + except: + obj_name = lside + ltype = "DefaultLock" + obj_name, ltype = obj_name.strip(), ltype.strip() + + if ltype not in ["DefaultLock","UseLock","EnterLock"]: + source_object.emit_to("Lock type '%s' not recognized." % ltype) + return + + obj = source_object.search_for_object(obj_name) + if not obj: + return + + obj_locks = obj.get_attribute_value("LOCKS") + + if "list" in switches or not switches: + if not obj_locks: + s = "There are no locks on %s." % obj.get_name() + else: + s = "Locks on %s:" % obj.get_name() + s += obj_locks.show() + source_object.emit_to(s) + return + + # we are trying to change things. Check permissions. + if not source_object.controls_other(obj): + source_object.emit_to(defines_global.NOCONTROL_MSG) + return + + if "del" in switches: + # clear a lock + if obj_locks: + if not obj_locks.has_type(ltype): + source_object.emit_to("No %s set on this object." % ltype) + else: + obj_locks.del_type(ltype) + obj.set_attribute("LOCKS", obj_locks) + source_object.emit_to("Cleared lock %s on %s." % (ltype, obj.get_name())) + else: + source_object.emit_to("No %s set on this object." % ltype) + return + else: + #try to add a lock + if not obj_locks: + obj_locks = locks.Locks() + if not keys: + #add an impassable lock + obj_locks.add_type(ltype, locks.Key()) + source_object.emit_to("Added impassable '%s' lock to %s." % (ltype, obj.get_name())) + else: + keys = [k.strip() for k in keys.split(",")] + okeys, gkeys, pkeys = [], [], [] + allgroups = [g.name for g in Group.objects.all()] + allperms = ["%s.%s" % (p.content_type.app_label, p.codename) for p in Permission.objects.all()] + for key in keys: + #differentiate different type of keys + if Object.objects.is_dbref(key): + okeys.append(key) + elif key in allgroups: + gkeys.append(key) + elif key in allperms: + pkeys.append(key) + else: + source_object.emit_to("Key '%s' is not recognized as a valid dbref, group or permission." % key) + return + # Create actual key objects from the respective lists + keys = [] + if okeys: + keys.append(locks.ObjKey(okeys)) + if gkeys: + keys.append(locks.GroupKey(gkeys)) + if pkeys: + keys.append(locks.PermKey(pkeys)) + #store the keys in the lock + obj_locks.add_type(ltype, keys) + kstring = "" + for key in keys: + kstring += " %s" % key + source_object.emit_to("Added lock '%s' to %s with keys%s." % (ltype, obj.get_name(), kstring)) + + obj.set_attribute("LOCKS",obj_locks) +GLOBAL_CMD_TABLE.add_command("@lock", cmd_lock, priv_tuple=("objects.create",),auto_help=True, staff_help=True) diff --git a/src/defines_global.py b/src/defines_global.py index c3c1db7dba..749ba24f31 100644 --- a/src/defines_global.py +++ b/src/defines_global.py @@ -23,10 +23,10 @@ OBJECT_TYPES = ( # These attribute names can't be modified by players. NOSET_ATTRIBS = ["MONEY", "ALIAS", "LASTPAGED", "__CHANLIST", "LAST", - "__PARENT", "LASTSITE"] + "__PARENT", "LASTSITE", "LOCKS"] # These attributes don't show up on objects when examined. -HIDDEN_ATTRIBS = ["__CHANLIST", "__PARENT"] +HIDDEN_ATTRIBS = ["__CHANLIST", "__PARENT", "LOCKS"] # Server version number. REVISION = os.popen('svnversion .', 'r').readline().strip() diff --git a/src/locks.py b/src/locks.py new file mode 100644 index 0000000000..2fb3b6b289 --- /dev/null +++ b/src/locks.py @@ -0,0 +1,246 @@ +""" +This module handles all in-game locks. + +A lock object contains a set of criteria (keys). When queried, the +lock tries the tested object/player against these criteria and returns +a True/False result. +""" +import traceback +from src.objects.models import Object +from src import logger + +class Key(object): + """ + This implements a lock key. + + Normally the Key is of OR type; if an object matches any criterion in the key, + the entire key is considered a match. With the 'exact' criterion, all criteria + contained in the key (except the list of object dbrefs) must also exist in the + object. + With the NOT flag the key is inversed, that is, only objects which do not + match the criterias (exact or not) will be considered to have access. + + Supplying no criteria will make the lock impassable (NOT flag results in an alvays open lock) + """ + def __init__(self, criteria=[], extra=[], NOT=False, exact=False): + """ + Defines the basic permission laws + permlist (list of strings) - permission definitions + grouplist (list of strings) - group names + objlist (list of obj or dbrefs) - match individual objects to the lock + NOT (bool) - invert the lock + exact (bool) - objects must match all criteria. Default is OR operation. + """ + self.criteria = criteria + self.extra = extra + + #set the boolean operators + self.NOT = not NOT + self.exact = exact + + # if we have no criteria, this is an impassable lock (or always open if NOT given). + self.impassable = not(criteria) + + def __str__(self): + s = "" + if not self.criteria: + s += " " + for crit in self.criteria: + s += " <%s>" % crit + return s.strip() + + def _result(self, result): + if self.exact: + result = result == len(self.criteria) + if result: + return self.NOT + else: + return not self.NOT + + def check(self, object): + """ + Compare the object to see if the key matches. + """ + if self.NOT: + return not self.impassable + return self.impassable + +class ObjKey(Key): + """ + This implements a Key matching against object id + """ + def check(self, object): + + if self.impassable: + return self.NOT + if object.dbref() in self.criteria: + return self.NOT + else: + return not self.NOT + +class GroupKey(Key): + """ + This key matches against group membership + """ + def check(self, object): + if self.impassable: return self.NOT + user = object.get_user_account() + if not user: return False + return self._result(len([g for g in user.groups.all() if str(g) in self.criteria])) + +class PermKey(Key): + """ + This key matches against permissions + """ + def check(self, object): + if self.impassable: return self.NOT + user = object.get_user_account() + if not user: return False + return self._result(len([p for p in self.criteria if object.has_perm(p)])) + +class FlagKey(Key): + """ + This key use a set of object flagss to define access. + """ + def check(self, object): + if self.impassable: return self.NOT + return self._result(len([f for f in self.criteria if object.has_flag(f)])) + +class AttrKey(Key): + """ + This key use a list of arbitrary attributes to define access. + + The attribute names are in the usual criteria. If there is a matching + list of values in the self.extra list we compare the values directly, + otherwise we just check for the existence of the attribute. + """ + def check(self, object): + if self.impassable: return self.NOT + val_list = self.extra + attr_list = self.criteria + if len(val_list) == len(attr_list): + return self._result(len([i for i in range(attr_list) + if object.get_attribute_value(attr_list[i])==val_list[i]])) + else: + return _result(len([a for a in attr_list if object.get_attribute_value(a)])) + +class Locks(object): + """ + The Locks object defines an overall grouping of Locks based after type. Each lock + contains a set of keys to limit access to a certain action. + The Lock object is stored in the attribute LOCKS on the object in question and the + engine queries it during the relevant situations. + + Below is a list copied from MUX. Currently Evennia only use 3 lock types: + Default, Use and Enter; it's not clear if any more are really needed. + + Name: Affects: Effect: + ----------------------------------------------------------------------- + DefaultLock: Exits: controls who may traverse the exit to + its destination. + Rooms: controls whether the player sees the SUCC + or FAIL message for the room following the + room description when looking at the room. + Players/Things: controls who may GET the object. + EnterLock: Players/Things: controls who may ENTER the object if the + object is ENTER_OK. Also, the enter lock + of an object being used as a Zone Master + Object determines control of that zone. + GetFromLock: All but Exits: controls who may gets things from a given + location. + GiveLock: Players/Things: controls who may give the object. + LeaveLock: Players/Things: controls who may LEAVE the object. + LinkLock: All but Exits: controls who may link to the location if the + location is LINK_OK (for linking exits or + setting drop-tos) or ABODE (for setting + homes) + MailLock: Players: controls who may @mail the player. + OpenLock: All but Exits: controls who may open an exit. + PageLock: Players: controls who may page the player. + ParentLock: All: controls who may make @parent links to the + object. + ReceiveLock: Players/Things: controls who may give things to the object. + SpeechLock: All but Exits: controls who may speak in that location + (only checked if AUDITORIUM flag is set + on that location) + TeloutLock: All but Exits: controls who may teleport out of the + location. + TportLock: Rooms/Things: controls who may teleport there if the + location is JUMP_OK. + UseLock: All but Exits: controls who may USE the object, GIVE the + object money and have the PAY attributes + run, have their messages heard and possibly + acted on by LISTEN and AxHEAR, and invoke + $-commands stored on the object. + DropLock: All but rooms: controls who may drop that object. + UserLock: All: Not used by MUX, is intended to be used + in MUX programming where a user-defined + lock is needed. + VisibleLock: All: Controls object visibility when the object + is not dark and the looker passes the lock. + In DARK locations, the object must also be + set LIGHT and the viewer must pass the + VisibleLock. + """ + + def __init__(self): + """ + + The Lock logic is strictly OR. If you want to make access restricted, + make it so in the respective Key. + """ + self.locks = {} + + def __str__(self): + s = "" + for lock in self.locks.keys(): + s += " %s" % lock + return s.strip() + + def add_type(self, ltype, keys=[]): + """ + type (string) : the type pf lock, like DefaultLock, UseLock etc. + keylist = list of Key objects defining who have access. + """ + if type(keys) != type(list()): + keys = [keys] + self.locks[ltype] = keys + + def del_type(self,ltype): + """ + Clears a lock. + """ + if self.has_type(ltype): + del self.locks[ltype] + + def has_type(self,ltype): + return self.locks.has_key(ltype) + + def show(self): + if not self.locks: + return "No locks." + s = "" + for lock, keys in self.locks.items(): + s += "\n %s\n " % lock + for key in keys: + s += " %s" % key + return s + + def check(self, ltype, object): + """ + This is called by the engine. It checks if this lock is of the right type, + and if so if there is access. If the type does not exist, there is no + lock for it and thus we return True. + """ + if not self.has_type(ltype): + return True + result = False + for key in self.locks[ltype]: + try: + result = result or key.check(object) + except KeyError: + pass + if not result and object.is_superuser(): + object.emit_to("Lock '%s' - Superuser override." % ltype) + return True + return result diff --git a/src/objects/models.py b/src/objects/models.py index 8f047b8c3a..f4eba06c0a 100755 --- a/src/objects/models.py +++ b/src/objects/models.py @@ -338,6 +338,22 @@ class Object(models.Model): # Fall through to failure return False + def has_group(self, group): + """ + Checks if a user is member of a particular user group. + """ + if not self.is_player(): + return False + + if self.is_superuser(): + return True + + if group in [g.name for g in self.get_user_account().groups.all()]: + return True + else: + return False + + def owns_other(self, other_obj): """ See if the envoked object owns another object. @@ -367,7 +383,7 @@ class Object(models.Model): # When builder_override is enabled, a builder permission means # the object controls the other. - if builder_override and not other_obj.is_player() and self.has_perm('genperms.builder'): + if builder_override and not other_obj.is_player() and self.has_group('Builders'): return True # They've failed to meet any of the above conditions. @@ -883,9 +899,18 @@ class Object(models.Model): quiet: (bool) If true, don't emit left/arrived messages. force_look: (bool) If true and self is a player, make them 'look'. """ - + + #first, check if we can enter that location at all. + if not target.scriptlink.enter_lock(self): + lock_desc = self.get_attribute_value("enter_lock_msg") + if lock_desc: + self.emit_to(lock_desc) + else: + self.emit_to("That destination is blocked from you.") + return + #before the move, call eventual pre-commands. - if self.scriptlink.at_before_move(target) != None: + if self.scriptlink.at_before_move(target) != None: return if not quiet: diff --git a/src/script_parents/basicobject.py b/src/script_parents/basicobject.py index b29b33e999..2ddb6146f4 100644 --- a/src/script_parents/basicobject.py +++ b/src/script_parents/basicobject.py @@ -135,16 +135,24 @@ class EvenniaBasicObject(object): # This is the object being looked at. target_obj = self.scripted_obj # See if the envoker sees dbref numbers. + lock_msg = "" if pobject: - show_dbrefs = pobject.sees_dbrefs() + show_dbrefs = pobject.sees_dbrefs() + + #check for the defaultlock, this shows a lock message after the normal desc, if one is defined. + if target_obj.is_room() and \ + not target_obj.scriptlink.default_lock(pobject): + temp = target_obj.get_attribute_value("lock_msg") + if temp: + lock_msg = "\n%s" % temp else: show_dbrefs = False - + description = target_obj.get_attribute_value('desc') if description is not None: - retval = "%s\r\n%s" % ( + retval = "%s\r\n%s%s" % ( target_obj.get_name(show_dbref=show_dbrefs), - target_obj.get_attribute_value('desc'), + target_obj.get_attribute_value('desc'), lock_msg ) else: retval = "%s" % ( @@ -192,8 +200,11 @@ class EvenniaBasicObject(object): values: * pobject: (Object) The object requesting the action. """ - # Assume everyone passes the default lock by default. - return True + locks = self.scripted_obj.get_attribute_value("LOCKS") + if locks: + return locks.check("DefaultLock", pobject) + else: + return True def use_lock(self, pobject): """ @@ -204,8 +215,11 @@ class EvenniaBasicObject(object): values: * pobject: (Object) The object requesting the action. """ - # Assume everyone passes the use lock by default. - return True + locks = self.scripted_obj.get_attribute_value("LOCKS") + if locks: + return locks.check("UseLock", pobject) + else: + return True def enter_lock(self, pobject): """ @@ -216,5 +230,8 @@ class EvenniaBasicObject(object): values: * pobject: (Object) The object requesting the action. """ - # Assume everyone passes the enter lock by default. - return True + locks = self.scripted_obj.get_attribute_value("LOCKS") + if locks: + return locks.check("EnterLock", pobject) + else: + return True