Reshuffling the Evennia package into the new template paradigm.

This commit is contained in:
Griatch 2015-01-06 14:53:45 +01:00
parent 2846e64833
commit 2b3a32e447
371 changed files with 17250 additions and 304 deletions

1
lib/locks/__init__.py Normal file
View file

@ -0,0 +1 @@
# -*- coding: utf-8 -*-

526
lib/locks/lockfuncs.py Normal file
View file

@ -0,0 +1,526 @@
"""
This module provides a set of permission lock functions for use
with Evennia's permissions system.
To call these locks, make sure this module is included in the
settings tuple PERMISSION_FUNC_MODULES then define a lock on the form
'<access_type>:func(args)' and add it to the object's lockhandler.
Run the access() method of the handler to execute the lock check.
Note that accessing_obj and accessed_obj can be any object type
with a lock variable/field, so be careful to not expect
a certain object type.
Appendix: MUX locks
Below is a list nicked from the MUX help file on the locks available
in standard MUX. Most of these are not relevant to core Evennia since
locks in Evennia are considerably more flexible and can be implemented
on an individual command/typeclass basis rather than as globally
available like the MUX ones. So many of these are not available in
basic Evennia, but could all be implemented easily if needed for the
individual game.
MUX Name: Affects: Effect:
-------------------------------------------------------------------------------
DefaultLock: Exits: controls who may traverse the exit to
its destination.
Evennia: "traverse:<lockfunc()>"
Rooms: controls whether the player sees the SUCC
or FAIL message for the room following the
room description when looking at the room.
Evennia: Custom typeclass
Players/Things: controls who may GET the object.
Evennia: "get:<lockfunc()"
EnterLock: Players/Things: controls who may ENTER the object
Evennia:
GetFromLock: All but Exits: controls who may gets things from a given
location.
Evennia:
GiveLock: Players/Things: controls who may give the object.
Evennia:
LeaveLock: Players/Things: controls who may LEAVE the object.
Evennia:
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)
Evennia:
MailLock: Players: controls who may @mail the player.
Evennia:
OpenLock: All but Exits: controls who may open an exit.
Evennia:
PageLock: Players: controls who may page the player.
Evennia: "send:<lockfunc()>"
ParentLock: All: controls who may make @parent links to the
object.
Evennia: Typeclasses and "puppet:<lockstring()>"
ReceiveLock: Players/Things: controls who may give things to the object.
Evennia:
SpeechLock: All but Exits: controls who may speak in that location
Evennia:
TeloutLock: All but Exits: controls who may teleport out of the
location.
Evennia:
TportLock: Rooms/Things: controls who may teleport there
Evennia:
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.
Evennia: Commands and Cmdsets.
DropLock: All but rooms: controls who may drop that object.
Evennia:
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.
Evennia: Room typeclass with Dark/light script
"""
from django.conf import settings
from src.utils import utils
_PERMISSION_HIERARCHY = [p.lower() for p in settings.PERMISSION_HIERARCHY]
def _to_player(accessing_obj):
"Helper function. Makes sure an accessing object is a player object"
if utils.inherits_from(accessing_obj, "src.objects.objects.Object"):
# an object. Convert to player.
accessing_obj = accessing_obj.player
return accessing_obj
# lock functions
def true(*args, **kwargs):
"Always returns True."
return True
def all(*args, **kwargs):
return True
def false(*args, **kwargs):
"Always returns False"
return False
def none(*args, **kwargs):
return False
def self(accessing_obj, accessed_obj, *args, **kwargs):
"""
Check if accessing_obj is the same as accessed_obj
Usage:
self()
This can be used to lock specifically only to
the same object that the lock is defined on.
"""
return accessing_obj == accessed_obj
def perm(accessing_obj, accessed_obj, *args, **kwargs):
"""
The basic permission-checker. Ignores case.
Usage:
perm(<permission>)
where <permission> is the permission accessing_obj must
have in order to pass the lock.
If the given permission is part of settings.PERMISSION_HIERARCHY,
permission is also granted to all ranks higher up in the hierarchy.
If accessing_object is an Object controlled by a Player, the
permissions of the Player is used unless the Attribute _quell
is set to True on the Object. In this case however, the
LOWEST hieararcy-permission of the Player/Object-pair will be used
(this is order to avoid Players potentially escalating their own permissions
by use of a higher-level Object)
"""
# this allows the perm_above lockfunc to make use of this function too
gtmode = kwargs.pop("_greater_than", False)
try:
perm = args[0].lower()
perms_object = [p.lower() for p in accessing_obj.permissions.all()]
except (AttributeError, IndexError):
return False
if utils.inherits_from(accessing_obj, "src.objects.objects.Object") and accessing_obj.player:
player = accessing_obj.player
perms_player = [p.lower() for p in player.permissions.all()]
is_quell = player.attributes.get("_quell")
if perm in _PERMISSION_HIERARCHY:
# check hierarchy without allowing escalation obj->player
hpos_target = _PERMISSION_HIERARCHY.index(perm)
hpos_player = [hpos for hpos, hperm in enumerate(_PERMISSION_HIERARCHY) if hperm in perms_player]
hpos_player = hpos_player and hpos_player[-1] or -1
if is_quell:
hpos_object = [hpos for hpos, hperm in enumerate(_PERMISSION_HIERARCHY) if hperm in perms_object]
hpos_object = hpos_object and hpos_object[-1] or -1
if gtmode:
return hpos_target < min(hpos_player, hpos_object)
else:
return hpos_target <= min(hpos_player, hpos_object)
elif gtmode:
return gtmode and hpos_target < hpos_player
else:
return hpos_target <= hpos_player
elif not is_quell and perm in perms_player:
# if we get here, check player perms first, otherwise
# continue as normal
return True
if perm in perms_object:
# simplest case - we have direct match
return True
if perm in _PERMISSION_HIERARCHY:
# check if we have a higher hierarchy position
hpos_target = _PERMISSION_HIERARCHY.index(perm)
return any(1 for hpos, hperm in enumerate(_PERMISSION_HIERARCHY)
if hperm in perms_object and hpos_target < hpos)
return False
def perm_above(accessing_obj, accessed_obj, *args, **kwargs):
"""
Only allow objects with a permission *higher* in the permission
hierarchy than the one given. If there is no such higher rank,
it's assumed we refer to superuser. If no hierarchy is defined,
this function has no meaning and returns False.
"""
kwargs["_greater_than"] = True
return perm(accessing_obj, accessed_obj, *args, **kwargs)
def pperm(accessing_obj, accessed_obj, *args, **kwargs):
"""
The basic permission-checker only for Player objects. Ignores case.
Usage:
pperm(<permission>)
where <permission> is the permission accessing_obj must
have in order to pass the lock. If the given permission
is part of _PERMISSION_HIERARCHY, permission is also granted
to all ranks higher up in the hierarchy.
"""
return perm(_to_player(accessing_obj), accessed_obj, *args, **kwargs)
def pperm_above(accessing_obj, accessed_obj, *args, **kwargs):
"""
Only allow Player objects with a permission *higher* in the permission
hierarchy than the one given. If there is no such higher rank,
it's assumed we refer to superuser. If no hierarchy is defined,
this function has no meaning and returns False.
"""
return perm_above(_to_player(accessing_obj), accessed_obj, *args, **kwargs)
def dbref(accessing_obj, accessed_obj, *args, **kwargs):
"""
Usage:
dbref(3)
This lock type checks if the checking object
has a particular dbref. Note that this only
works for checking objects that are stored
in the database (e.g. not for commands)
"""
if not args:
return False
try:
dbref = int(args[0].strip().strip('#'))
except ValueError:
return False
if hasattr(accessing_obj, 'dbid'):
return dbref == accessing_obj.dbid
return False
def pdbref(accessing_obj, accessed_obj, *args, **kwargs):
"""
Same as dbref, but making sure accessing_obj is a player.
"""
return dbref(_to_player(accessing_obj), accessed_obj, *args, **kwargs)
def id(accessing_obj, accessed_obj, *args, **kwargs):
"Alias to dbref"
return dbref(accessing_obj, accessed_obj, *args, **kwargs)
def pid(accessing_obj, accessed_obj, *args, **kwargs):
"Alias to dbref, for Players"
return dbref(_to_player(accessing_obj), accessed_obj, *args, **kwargs)
# this is more efficient than multiple if ... elif statments
CF_MAPPING = {'eq': lambda val1, val2: val1 == val2 or int(val1) == int(val2),
'gt': lambda val1, val2: int(val1) > int(val2),
'lt': lambda val1, val2: int(val1) < int(val2),
'ge': lambda val1, val2: int(val1) >= int(val2),
'le': lambda val1, val2: int(val1) <= int(val2),
'ne': lambda val1, val2: int(val1) != int(val2),
'default': lambda val1, val2: False}
def attr(accessing_obj, accessed_obj, *args, **kwargs):
"""
Usage:
attr(attrname)
attr(attrname, value)
attr(attrname, value, compare=type)
where compare's type is one of (eq,gt,lt,ge,le,ne) and signifies
how the value should be compared with one on accessing_obj (so
compare=gt means the accessing_obj must have a value greater than
the one given).
Searches attributes *and* properties stored on the checking
object. The first form works like a flag - if the
attribute/property exists on the object, the value is checked for
True/False. The second form also requires that the value of the
attribute/property matches. Note that all retrieved values will be
converted to strings before doing the comparison.
"""
# deal with arguments
if not args:
return False
attrname = args[0].strip()
value = None
if len(args) > 1:
value = args[1].strip()
compare = 'eq'
if kwargs:
compare = kwargs.get('compare', 'eq')
def valcompare(val1, val2, typ='eq'):
"compare based on type"
try:
return CF_MAPPING.get(typ, 'default')(val1, val2)
except Exception:
# this might happen if we try to compare two things
# that cannot be compared
return False
# first, look for normal properties on the object trying to gain access
if hasattr(accessing_obj, attrname):
if value:
return valcompare(str(getattr(accessing_obj, attrname)), value, compare)
# will return Fail on False value etc
return bool(getattr(accessing_obj, attrname))
# check attributes, if they exist
if (hasattr(accessing_obj, 'attributes') and accessing_obj.attributes.has(attrname)):
if value:
return (hasattr(accessing_obj, 'attributes')
and valcompare(accessing_obj.attributes.get(attrname), value, compare))
# fails on False/None values
return bool(accessing_obj.attributes.get(attrname))
return False
def objattr(accessing_obj, accessed_obj, *args, **kwargs):
"""
Usage:
objattr(attrname)
objattr(attrname, value)
objattr(attrname, value, compare=type)
Works like attr, except it looks for an attribute on
accessing_obj.obj, if such an entity exists. Suitable
for commands.
"""
if hasattr(accessing_obj, "obj"):
return attr(accessing_obj.obj, accessed_obj, *args, **kwargs)
def locattr(accessing_obj, accessed_obj, *args, **kwargs):
"""
Usage:
locattr(attrname)
locattr(attrname, value)
locattr(attrname, value, compare=type)
Works like attr, except it looks for an attribute on
accessing_obj.location, if such an entity exists.
"""
if hasattr(accessing_obj, "location"):
return attr(accessing_obj.location, accessed_obj, *args, **kwargs)
def attr_eq(accessing_obj, accessed_obj, *args, **kwargs):
"""
Usage:
attr_gt(attrname, 54)
"""
return attr(accessing_obj, accessed_obj, *args, **kwargs)
def attr_gt(accessing_obj, accessed_obj, *args, **kwargs):
"""
Usage:
attr_gt(attrname, 54)
Only true if access_obj's attribute > the value given.
"""
return attr(accessing_obj, accessed_obj, *args, **{'compare': 'gt'})
def attr_ge(accessing_obj, accessed_obj, *args, **kwargs):
"""
Usage:
attr_gt(attrname, 54)
Only true if access_obj's attribute >= the value given.
"""
return attr(accessing_obj, accessed_obj, *args, **{'compare': 'ge'})
def attr_lt(accessing_obj, accessed_obj, *args, **kwargs):
"""
Usage:
attr_gt(attrname, 54)
Only true if access_obj's attribute < the value given.
"""
return attr(accessing_obj, accessed_obj, *args, **{'compare': 'lt'})
def attr_le(accessing_obj, accessed_obj, *args, **kwargs):
"""
Usage:
attr_gt(attrname, 54)
Only true if access_obj's attribute <= the value given.
"""
return attr(accessing_obj, accessed_obj, *args, **{'compare': 'le'})
def attr_ne(accessing_obj, accessed_obj, *args, **kwargs):
"""
Usage:
attr_gt(attrname, 54)
Only true if access_obj's attribute != the value given.
"""
return attr(accessing_obj, accessed_obj, *args, **{'compare': 'ne'})
def inside(accessing_obj, accessed_obj, *args, **kwargs):
"""
Usage:
inside()
Only true if accessing_obj is "inside" accessed_obj
"""
return accessing_obj.location == accessed_obj
def holds(accessing_obj, accessed_obj, *args, **kwargs):
"""
Usage:
holds() checks if accessed_obj or accessed_obj.obj
is held by accessing_obj
holds(key/dbref) checks if accessing_obj holds an object
with given key/dbref
holds(attrname, value) checks if accessing_obj holds an
object with the given attrname and value
This is passed if accessed_obj is carried by accessing_obj (that is,
accessed_obj.location == accessing_obj), or if accessing_obj itself holds
an object matching the given key.
"""
try:
# commands and scripts don't have contents, so we are usually looking
# for the contents of their .obj property instead (i.e. the object the
# command/script is attached to).
contents = accessing_obj.contents
except AttributeError:
try:
contents = accessing_obj.obj.contents
except AttributeError:
return False
def check_holds(objid):
# helper function. Compares both dbrefs and keys/aliases.
objid = str(objid)
dbref = utils.dbref(objid, reqhash=False)
if dbref and any((True for obj in contents if obj.dbid == dbref)):
return True
objid = objid.lower()
return any((True for obj in contents
if obj.key.lower() == objid or objid in [al.lower() for al in obj.aliases.all()]))
if not args:
# holds() - check if accessed_obj or accessed_obj.obj is held by accessing_obj
try:
if check_holds(accessed_obj.dbid):
return True
except Exception:
pass
return hasattr(accessed_obj, "obj") and check_holds(accessed_obj.obj.dbid)
if len(args) == 1:
# command is holds(dbref/key) - check if given objname/dbref is held by accessing_ob
return check_holds(args[0])
elif len(args = 2):
# command is holds(attrname, value) check if any held object has the given attribute and value
for obj in contents:
if obj.attributes.get(args[0]) == args[1]:
return True
def superuser(*args, **kwargs):
"""
Only accepts an accesing_obj that is superuser (e.g. user #1)
Since a superuser would not ever reach this check (superusers
bypass the lock entirely), any user who gets this far cannot be a
superuser, hence we just return False. :)
"""
return False
def serversetting(accessing_obj, accessed_obj, *args, **kwargs):
"""
Only returns true if the Evennia settings exists, alternatively has
a certain value.
Usage:
serversetting(IRC_ENABLED)
serversetting(BASE_SCRIPT_PATH, [game.gamesrc.scripts])
A given True/False or integers will be converted properly.
"""
if not args or not args[0]:
return False
if len(args) < 2:
setting = args[0]
val = "True"
else:
setting, val = args[0], args[1]
# convert
if val == 'True':
val = True
elif val == 'False':
val = False
elif val.isdigit():
val = int(val)
if setting in settings._wrapped.__dict__:
return settings._wrapped.__dict__[setting] == val
return False

439
lib/locks/lockhandler.py Normal file
View file

@ -0,0 +1,439 @@
"""
Locks
A lock defines access to a particular subsystem or property of
Evennia. For example, the "owner" property can be impmemented as a
lock. Or the disability to lift an object or to ban users.
A lock consists of three parts:
- access_type - this defines what kind of access this lock regulates. This
just a string.
- function call - this is one or many calls to functions that will determine
if the lock is passed or not.
- lock function(s). These are regular python functions with a special
set of allowed arguments. They should always return a boolean depending
on if they allow access or not.
# Lock function
A lock function is defined by existing in one of the modules
listed by settings.LOCK_FUNC_MODULES. It should also always
take four arguments looking like this:
funcname(accessing_obj, accessed_obj, *args, **kwargs):
[...]
The accessing object is the object wanting to gain access.
The accessed object is the object this lock resides on
args and kwargs will hold optional arguments and/or keyword arguments
to the function as a list and a dictionary respectively.
Example:
perm(accessing_obj, accessed_obj, *args, **kwargs):
"Checking if the object has a particular, desired permission"
if args:
desired_perm = args[0]
return desired_perm in accessing_obj.permissions.all()
return False
Lock functions should most often be pretty general and ideally possible to
re-use and combine in various ways to build clever locks.
# Lock definition ("Lock string")
A lock definition is a string with a special syntax. It is added to
each object's lockhandler, making that lock available from then on.
The lock definition looks like this:
'access_type:[NOT] func1(args)[ AND|OR][NOT] func2() ...'
That is, the access_type, a colon followed by calls to lock functions
combined with AND or OR. NOT negates the result of the following call.
Example:
We want to limit who may edit a particular object (let's call this access_type
for 'edit', it depends on what the command is looking for). We want this to
only work for those with the Permission 'Builders'. So we use our lock
function above and define it like this:
'edit:perm(Builders)'
Here, the lock-function perm() will be called with the string
'Builders' (accessing_obj and accessed_obj are added automatically,
you only need to add the args/kwargs, if any).
If we wanted to make sure the accessing object was BOTH a Builders and a
GoodGuy, we could use AND:
'edit:perm(Builders) AND perm(GoodGuy)'
To allow EITHER Builders and GoodGuys, we replace AND with OR. perm() is just
one example, the lock function can do anything and compare any properties of
the calling object to decide if the lock is passed or not.
'lift:attrib(very_strong) AND NOT attrib(bad_back)'
To make these work, add the string to the lockhandler of the object you want
to apply the lock to:
obj.lockhandler.add('edit:perm(Builders)')
From then on, a command that wants to check for 'edit' access on this
object would do something like this:
if not target_obj.lockhandler.has_perm(caller, 'edit'):
caller.msg("Sorry, you cannot edit that.")
All objects also has a shortcut called 'access' that is recommended to
use instead:
if not target_obj.access(caller, 'edit'):
caller.msg("Sorry, you cannot edit that.")
# Permissions
Permissions are just text strings stored in a comma-separated list on
typeclassed objects. The default perm() lock function uses them,
taking into account settings.PERMISSION_HIERARCHY. Also, the
restricted @perm command sets them, but otherwise they are identical
to any other identifier you can use.
"""
import re
import inspect
from django.conf import settings
from src.utils import logger, utils
from django.utils.translation import ugettext as _
__all__ = ("LockHandler", "LockException")
WARNING_LOG = "lockwarnings.log"
#
# Exception class. This will be raised
# by errors in lock definitions.
#
class LockException(Exception):
"raised during an error in a lock."
pass
#
# Cached lock functions
#
_LOCKFUNCS = {}
def _cache_lockfuncs():
"Updates the cache."
global _LOCKFUNCS
_LOCKFUNCS = {}
for modulepath in settings.LOCK_FUNC_MODULES:
modulepath = utils.pypath_to_realpath(modulepath)
mod = utils.mod_import(modulepath)
if mod:
for tup in (tup for tup in inspect.getmembers(mod) if callable(tup[1])):
_LOCKFUNCS[tup[0]] = tup[1]
else:
logger.log_errmsg("Couldn't load %s from PERMISSION_FUNC_MODULES." % modulepath)
#
# pre-compiled regular expressions
#
_RE_FUNCS = re.compile(r"\w+\([^)]*\)")
_RE_SEPS = re.compile(r"(?<=[ )])AND(?=\s)|(?<=[ )])OR(?=\s)|(?<=[ )])NOT(?=\s)")
_RE_OK = re.compile(r"%s|and|or|not")
#
#
# Lock handler
#
#
class LockHandler(object):
"""
This handler should be attached to all objects implementing
permission checks, under the property 'lockhandler'.
"""
def __init__(self, obj):
"""
Loads and pre-caches all relevant locks and their
functions.
"""
if not _LOCKFUNCS:
_cache_lockfuncs()
self.obj = obj
self.locks = {}
self.reset()
def __str__(self):
return ";".join(self.locks[key][2] for key in sorted(self.locks))
def _log_error(self, message):
"Try to log errors back to object"
raise LockException(message)
def _parse_lockstring(self, storage_lockstring):
"""
Helper function. This is normally only called when the
lockstring is cached and does preliminary checking. locks are
stored as a string
'atype:[NOT] lock()[[ AND|OR [NOT] lock()[...]];atype...
"""
locks = {}
if not storage_lockstring:
return locks
duplicates = 0
elist = [] # errors
wlist = [] # warnings
for raw_lockstring in storage_lockstring.split(';'):
if not raw_lockstring:
continue
lock_funcs = []
try:
access_type, rhs = (part.strip() for part in raw_lockstring.split(':', 1))
except ValueError:
logger.log_trace()
return locks
# parse the lock functions and separators
funclist = _RE_FUNCS.findall(rhs)
evalstring = rhs
for pattern in ('AND', 'OR', 'NOT'):
evalstring = re.sub(r"\b%s\b" % pattern, pattern.lower(), evalstring)
nfuncs = len(funclist)
for funcstring in funclist:
funcname, rest = (part.strip().strip(')') for part in funcstring.split('(', 1))
func = _LOCKFUNCS.get(funcname, None)
if not callable(func):
elist.append(_("Lock: lock-function '%s' is not available.") % funcstring)
continue
args = list(arg.strip() for arg in rest.split(',') if arg and not '=' in arg)
kwargs = dict([arg.split('=', 1) for arg in rest.split(',') if arg and '=' in arg])
lock_funcs.append((func, args, kwargs))
evalstring = evalstring.replace(funcstring, '%s')
if len(lock_funcs) < nfuncs:
continue
try:
# purge the eval string of any superfluous items, then test it
evalstring = " ".join(_RE_OK.findall(evalstring))
eval(evalstring % tuple(True for func in funclist), {}, {})
except Exception:
elist.append(_("Lock: definition '%s' has syntax errors.") % raw_lockstring)
continue
if access_type in locks:
duplicates += 1
wlist.append(_("LockHandler on %(obj)s: access type '%(access_type)s' changed from '%(source)s' to '%(goal)s' " % \
{"obj":self.obj, "access_type":access_type, "source":locks[access_type][2], "goal":raw_lockstring}))
locks[access_type] = (evalstring, tuple(lock_funcs), raw_lockstring)
if wlist:
# a warning text was set, it's not an error, so only report
logger.log_file("\n".join(wlist), WARNING_LOG)
if elist:
# an error text was set, raise exception.
raise LockException("\n".join(elist))
# return the gathered locks in an easily executable form
return locks
def _cache_locks(self, storage_lockstring):
"""Store data"""
self.locks = self._parse_lockstring(storage_lockstring)
def _save_locks(self):
"Store locks to obj"
self.obj.lock_storage = ";".join([tup[2] for tup in self.locks.values()])
def cache_lock_bypass(self, obj):
"""
We cache superuser bypass checks here for efficiency. This needs to
be re-run when a player is assigned to a character.
We need to grant access to superusers. We need to check both directly
on the object (players), through obj.player and using the get_player()
method (this sits on serversessions, in some rare cases where a
check is done before the login process has yet been fully finalized)
"""
self.lock_bypass = hasattr(obj, "is_superuser") and obj.is_superuser
def add(self, lockstring):
"""
Add a new lockstring on the form '<access_type>:<functions>'. Multiple
access types should be separated by semicolon (;).
"""
# sanity checks
for lockdef in lockstring.split(';'):
if not ':' in lockstring:
self._log_error(_("Lock: '%s' contains no colon (:).") % lockdef)
return False
access_type, rhs = [part.strip() for part in lockdef.split(':', 1)]
if not access_type:
self._log_error(_("Lock: '%s' has no access_type (left-side of colon is empty).") % lockdef)
return False
if rhs.count('(') != rhs.count(')'):
self._log_error(_("Lock: '%s' has mismatched parentheses.") % lockdef)
return False
if not _RE_FUNCS.findall(rhs):
self._log_error(_("Lock: '%s' has no valid lock functions.") % lockdef)
return False
# get the lock string
storage_lockstring = self.obj.lock_storage
if storage_lockstring:
storage_lockstring = storage_lockstring + ";" + lockstring
else:
storage_lockstring = lockstring
# cache the locks will get rid of eventual doublets
self._cache_locks(storage_lockstring)
self._save_locks()
return True
def replace(self, lockstring):
"Replaces the lockstring entirely."
old_lockstring = str(self)
self.clear()
try:
return self.add(lockstring)
except LockException:
self.add(old_lockstring)
raise
def get(self, access_type=None):
"get the full lockstring or the lockstring of a particular access type."
if access_type:
return self.locks.get(access_type, ["", "", ""])[2]
return str(self)
def delete(self, access_type):
"Remove a lock from the handler"
if access_type in self.locks:
del self.locks[access_type]
self._save_locks()
return True
return False
def clear(self):
"Remove all locks"
self.locks = {}
self.lock_storage = ""
self._save_locks()
def reset(self):
"""
Set the reset flag, so the the lock will be re-cached at next checking.
This is usually set by @reload.
"""
self._cache_locks(self.obj.lock_storage)
self.cache_lock_bypass(self.obj)
def check(self, accessing_obj, access_type, default=False, no_superuser_bypass=False):
"""
Checks a lock of the correct type by passing execution
off to the lock function(s).
accessing_obj - the object seeking access
access_type - the type of access wanted
default - if no suitable lock type is found, use this
no_superuser_bypass - don't use this unless you really, really need to,
it makes supersusers susceptible to the lock check.
A lock is executed in the follwoing way:
Parsing the lockstring, we (during cache) extract the valid
lock functions and store their function objects in the right
order along with their args/kwargs. These are now executed in
sequence, creating a list of True/False values. This is put
into the evalstring, which is a string of AND/OR/NOT entries
separated by placeholders where each function result should
go. We just put those results in and evaluate the string to
get a final, combined True/False value for the lockstring.
The important bit with this solution is that the full
lockstring is never blindly evaluated, and thus there (should
be) no way to sneak in malign code in it. Only "safe" lock
functions (as defined by your settings) are executed.
"""
try:
# check if the lock should be bypassed (e.g. superuser status)
if accessing_obj.locks.lock_bypass and not no_superuser_bypass:
return True
except AttributeError:
# happens before session is initiated.
if not no_superuser_bypass and ((hasattr(accessing_obj, 'is_superuser') and accessing_obj.is_superuser)
or (hasattr(accessing_obj, 'player') and hasattr(accessing_obj.player, 'is_superuser') and accessing_obj.player.is_superuser)
or (hasattr(accessing_obj, 'get_player') and (not accessing_obj.get_player() or accessing_obj.get_player().is_superuser))):
return True
# no superuser or bypass -> normal lock operation
if access_type in self.locks:
# we have a lock, test it.
evalstring, func_tup, raw_string = self.locks[access_type]
# execute all lock funcs in the correct order, producing a tuple of True/False results.
true_false = tuple(bool(tup[0](accessing_obj, self.obj, *tup[1], **tup[2])) for tup in func_tup)
# the True/False tuple goes into evalstring, which combines them
# with AND/OR/NOT in order to get the final result.
return eval(evalstring % true_false)
else:
return default
def check_lockstring(self, accessing_obj, lockstring, no_superuser_bypass=False):
"""
Do a direct check against a lockstring ('atype:func()..'), without any
intermediary storage on the accessed object (this can be left
to None if the lock functions called don't access it). atype can also be
put to a dummy value since no lock selection is made.
"""
try:
if accessing_obj.locks.lock_bypass and not no_superuser_bypass:
return True
except AttributeError:
if no_superuser_bypass and ((hasattr(accessing_obj, 'is_superuser') and accessing_obj.is_superuser)
or (hasattr(accessing_obj, 'player') and hasattr(accessing_obj.player, 'is_superuser') and accessing_obj.player.is_superuser)
or (hasattr(accessing_obj, 'get_player') and (not accessing_obj.get_player() or accessing_obj.get_player().is_superuser))):
return True
locks = self._parse_lockstring(lockstring)
for access_type in locks:
evalstring, func_tup, raw_string = locks[access_type]
true_false = tuple(tup[0](accessing_obj, self.obj, *tup[1],**tup[2])
for tup in func_tup)
return eval(evalstring % true_false)
def _test():
# testing
class TestObj(object):
pass
import pdb
obj1 = TestObj()
obj2 = TestObj()
#obj1.lock_storage = "owner:dbref(#4);edit:dbref(#5) or perm(Wizards);examine:perm(Builders);delete:perm(Wizards);get:all()"
#obj1.lock_storage = "cmd:all();admin:id(1);listen:all();send:all()"
obj1.lock_storage = "listen:perm(Immortals)"
pdb.set_trace()
obj1.locks = LockHandler(obj1)
obj2.permissions.add("Immortals")
obj2.id = 4
#obj1.locks.add("edit:attr(test)")
print "comparing obj2.permissions (%s) vs obj1.locks (%s)" % (obj2.permissions, obj1.locks)
print obj1.locks.check(obj2, 'owner')
print obj1.locks.check(obj2, 'edit')
print obj1.locks.check(obj2, 'examine')
print obj1.locks.check(obj2, 'delete')
print obj1.locks.check(obj2, 'get')
print obj1.locks.check(obj2, 'listen')

63
lib/locks/tests.py Normal file
View file

@ -0,0 +1,63 @@
# -*- coding: utf-8 -*-
"""
This is part of Evennia's unittest framework, for testing
the stability and integrrity of the codebase during updates.
This module tests the lock functionality of Evennia.
"""
try:
# this is a special optimized Django version, only available in current Django devel
from django.utils.unittest import TestCase
except ImportError:
from django.test import TestCase
from django.conf import settings
from src.locks import lockfuncs
from src.utils import create
#------------------------------------------------------------
#
# Lock testing
#
#------------------------------------------------------------
class LockTest(TestCase):
"Defines the lock test base"
def setUp(self):
"sets up the testing environment"
self.obj1 = create.create_object(settings.BASE_OBJECT_TYPECLASS, key="obj1")
self.obj2 = create.create_object(settings.BASE_OBJECT_TYPECLASS, key="obj2")
class TestLockCheck(LockTest):
def testrun(self):
dbref = self.obj2.dbref
self.obj1.locks.add("owner:dbref(%s);edit:dbref(%s) or perm(Wizards);examine:perm(Builders) and id(%s);delete:perm(Wizards);get:all()" % (dbref, dbref, dbref))
self.obj2.permissions.add('Wizards')
self.assertEquals(True, self.obj1.locks.check(self.obj2, 'owner'))
self.assertEquals(True, self.obj1.locks.check(self.obj2, 'edit'))
self.assertEquals(True, self.obj1.locks.check(self.obj2, 'examine'))
self.assertEquals(True, self.obj1.locks.check(self.obj2, 'delete'))
self.assertEquals(True, self.obj1.locks.check(self.obj2, 'get'))
self.obj1.locks.add("get:false()")
self.assertEquals(False, self.obj1.locks.check(self.obj2, 'get'))
self.assertEquals(True, self.obj1.locks.check(self.obj2, 'not_exist', default=True))
class TestLockfuncs(LockTest):
def testrun(self):
self.obj2.permissions.add('Wizards')
self.assertEquals(True, lockfuncs.true(self.obj2, self.obj1))
self.assertEquals(False, lockfuncs.false(self.obj2, self.obj1))
self.assertEquals(True, lockfuncs.perm(self.obj2, self.obj1, 'Wizards'))
self.assertEquals(True, lockfuncs.perm_above(self.obj2, self.obj1, 'Builders'))
dbref = self.obj2.dbref
self.assertEquals(True, lockfuncs.dbref(self.obj2, self.obj1, '%s' % dbref))
self.obj2.db.testattr = 45
self.assertEquals(True, lockfuncs.attr(self.obj2, self.obj1, 'testattr', '45'))
self.assertEquals(False, lockfuncs.attr_gt(self.obj2, self.obj1, 'testattr', '45'))
self.assertEquals(True, lockfuncs.attr_ge(self.obj2, self.obj1, 'testattr', '45'))
self.assertEquals(False, lockfuncs.attr_lt(self.obj2, self.obj1, 'testattr', '45'))
self.assertEquals(True, lockfuncs.attr_le(self.obj2, self.obj1, 'testattr', '45'))
self.assertEquals(False, lockfuncs.attr_ne(self.obj2, self.obj1, 'testattr', '45'))