Reshuffling the Evennia package into the new template paradigm.
This commit is contained in:
parent
2846e64833
commit
2b3a32e447
371 changed files with 17250 additions and 304 deletions
1
lib/locks/__init__.py
Normal file
1
lib/locks/__init__.py
Normal file
|
|
@ -0,0 +1 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
526
lib/locks/lockfuncs.py
Normal file
526
lib/locks/lockfuncs.py
Normal 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
439
lib/locks/lockhandler.py
Normal 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
63
lib/locks/tests.py
Normal 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'))
|
||||
Loading…
Add table
Add a link
Reference in a new issue