Trunk: Merged the Devel-branch (branches/griatch) into /trunk. This constitutes a major refactoring of Evennia. Development will now continue in trunk. See the wiki and the past posts to the mailing list for info. /Griatch
This commit is contained in:
parent
df29defbcd
commit
f83c2bddf8
222 changed files with 22304 additions and 14371 deletions
0
src/permissions/__init__.py
Normal file
0
src/permissions/__init__.py
Normal file
18
src/permissions/admin.py
Normal file
18
src/permissions/admin.py
Normal file
|
|
@ -0,0 +1,18 @@
|
|||
#
|
||||
# This sets up how models are displayed
|
||||
# in the web admin interface.
|
||||
#
|
||||
|
||||
from src.permissions.models import PermissionGroup
|
||||
from django.contrib import admin
|
||||
|
||||
class PermissionGroupAdmin(admin.ModelAdmin):
|
||||
list_display = ('id', 'db_key', 'db_group_permissions')
|
||||
list_display_links = ('id', "db_key")
|
||||
ordering = ['db_key', 'db_group_permissions']
|
||||
readonly_fields = ['db_key', 'db_group_permissions', 'db_permissions']
|
||||
search_fields = ['db_key']
|
||||
save_as = True
|
||||
save_on_top = True
|
||||
list_select_related = True
|
||||
admin.site.register(PermissionGroup, PermissionGroupAdmin)
|
||||
100
src/permissions/default_permissions.py
Normal file
100
src/permissions/default_permissions.py
Normal file
|
|
@ -0,0 +1,100 @@
|
|||
"""
|
||||
Setup the permission hierarchy and groups. This is
|
||||
read once during server startup. Further groups and
|
||||
permissions have to be added manually.
|
||||
|
||||
To set up your own permission scheme, have
|
||||
PERMISSION_SETUP_MODULE in game/settings point to
|
||||
a module of your own. This module must define two global
|
||||
dictionaries PERMS and GROUPS.
|
||||
|
||||
PERMS contains all permissions defined at server start
|
||||
on the form {key:desc, key:desc, ...}
|
||||
GROUPS gathers permissions (which must have been
|
||||
previously created as keys in PERMS) into clusters
|
||||
on the form {groupname: [key, key, ...], ...}
|
||||
"""
|
||||
|
||||
# Defining all permissions.
|
||||
PERMS = [
|
||||
'emit',
|
||||
'wall',
|
||||
|
||||
'teleport',
|
||||
'setobjalias',
|
||||
'wipe',
|
||||
'set',
|
||||
'cpattr',
|
||||
'mvattr',
|
||||
'find',
|
||||
'create',
|
||||
'copy',
|
||||
'open',
|
||||
'link',
|
||||
'unlink',
|
||||
'dig',
|
||||
'desc',
|
||||
'destroy',
|
||||
'examine',
|
||||
'typeclass',
|
||||
'debug',
|
||||
|
||||
'batchcommands',
|
||||
'batchcodes',
|
||||
|
||||
'ccreate',
|
||||
'cdesc',
|
||||
'tell',
|
||||
'time',
|
||||
'list',
|
||||
|
||||
'ps',
|
||||
'stats',
|
||||
|
||||
'reload',
|
||||
'py',
|
||||
'listscripts',
|
||||
'listcmdsets',
|
||||
'listobjects',
|
||||
'boot',
|
||||
'delplayer',
|
||||
'newpassword',
|
||||
'home',
|
||||
'service',
|
||||
'shutdown',
|
||||
'perm',
|
||||
'sethelp',
|
||||
]
|
||||
|
||||
# Permission Groups
|
||||
# Permission groups clump the previously defined permissions into
|
||||
# larger chunks. {groupname: [permissionkey,... ]}
|
||||
|
||||
GROUPS = {
|
||||
"Immortals": PERMS,
|
||||
"Wizards": [perm for perm in PERMS
|
||||
if perm not in ['shutdown',
|
||||
'py',
|
||||
'reload',
|
||||
'service',
|
||||
'perm',
|
||||
'batchcommands',
|
||||
'batchcodes']],
|
||||
|
||||
"Builders": [perm for perm in PERMS
|
||||
if perm not in ['shutdown',
|
||||
'py',
|
||||
'reload',
|
||||
'service',
|
||||
'perm',
|
||||
'batchcommands',
|
||||
'batchcodes',
|
||||
|
||||
'wall',
|
||||
'boot',
|
||||
'delplayer',
|
||||
'newpassword']],
|
||||
"PlayerHelpers": ['tell',
|
||||
'sethelp', 'ccreate', 'use_channels'],
|
||||
"Players": ['tell', 'ccreate', 'use_channels']
|
||||
}
|
||||
188
src/permissions/lockfunc_default.py
Normal file
188
src/permissions/lockfunc_default.py
Normal file
|
|
@ -0,0 +1,188 @@
|
|||
"""
|
||||
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 permission
|
||||
string of the form 'myfunction(myargs)' and store it in the
|
||||
'permissions' field or variable on your object/command/channel/whatever.
|
||||
As part of the permission check, such permission strings will be
|
||||
evaluated to call myfunction(checking_obj, checked_obj, *yourargs) in
|
||||
this module. A boolean value is expected back.
|
||||
|
||||
Note that checking_obj and checked_obj can be any object type
|
||||
with a permissions variable/field, so be careful to not expect
|
||||
a certain object type.
|
||||
|
||||
|
||||
MUX locks
|
||||
|
||||
Below is a list nicked from the MUX docs on the locks available
|
||||
in MUX. These are not all necessarily relevant to an Evennia game
|
||||
but to show they are all possible with Evennia, each entry is a
|
||||
suggestion on how one could implement similar functionality in Evennia.
|
||||
|
||||
Name: Affects: Effect:
|
||||
-------------------------------------------------------------------------
|
||||
DefaultLock: Exits: controls who may traverse the exit to
|
||||
its destination.
|
||||
Evennia: specialized permission key
|
||||
'traverse' checked in move method
|
||||
Rooms: controls whether the player sees the SUCC
|
||||
or FAIL message for the room following the
|
||||
room description when looking at the room.
|
||||
Evennia: This is better done by implementing
|
||||
a clever room class ...
|
||||
Players/Things: controls who may GET the object.
|
||||
Evennia: specialized permission key 'get'
|
||||
defined on object, checked by get command
|
||||
EnterLock: Players/Things: controls who may ENTER the object
|
||||
Evennia: specialized permission key 'enter'
|
||||
defined on object, checked by move command
|
||||
GetFromLock: All but Exits: controls who may gets things from a given
|
||||
location.
|
||||
Evennia: Probably done best with a lock function
|
||||
that searches the database for permitted users
|
||||
GiveLock: Players/Things: controls who may give the object.
|
||||
Evennia: specialized permission key 'give'
|
||||
checked by the give command
|
||||
LeaveLock: Players/Things: controls who may LEAVE the object.
|
||||
Evennia: specialized permission key 'leave'
|
||||
checked by move command
|
||||
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: specialized permission key 'link'
|
||||
set on obj and checked by link command
|
||||
MailLock: Players: controls who may @mail the player.
|
||||
Evennia: Lock function that pulls the
|
||||
config from the player to see if the
|
||||
calling player is on the blacklist/whitelist
|
||||
OpenLock: All but Exits: controls who may open an exit.
|
||||
Evennia: specialized permission key 'open'
|
||||
set on exit, checked by open command
|
||||
PageLock: Players: controls who may page the player.
|
||||
Evennia: see Maillock
|
||||
ParentLock: All: controls who may make @parent links to the
|
||||
object.
|
||||
Evennia: This is handled with typeclasses
|
||||
and typeclass switching instead.
|
||||
ReceiveLock: Players/Things: controls who may give things to the object.
|
||||
Evennia: See GiveLock
|
||||
SpeechLock: All but Exits: controls who may speak in that location
|
||||
Evennia: Lock function checking if there
|
||||
is some special restrictions on the room
|
||||
(game dependent)
|
||||
TeloutLock: All but Exits: controls who may teleport out of the
|
||||
location.
|
||||
Evennia: See LeaveLock
|
||||
TportLock: Rooms/Things: controls who may teleport there
|
||||
Evennia: See EnterLock
|
||||
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: Implemented per game
|
||||
DropLock: All but rooms: controls who may drop that object.
|
||||
Evennia: specialized permission key 'drop'
|
||||
set on room, checked by drop command.
|
||||
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: Better done with Scripts implementing
|
||||
a dark state/cmdset. For a single object,
|
||||
use a specialized permission key 'visible'
|
||||
set on object and checked by look command.
|
||||
|
||||
"""
|
||||
|
||||
from src.permissions.permissions import get_types, has_perm, has_perm_string
|
||||
from src.utils import search
|
||||
|
||||
|
||||
def noperm(checking_obj, checked_obj, *args):
|
||||
"""
|
||||
Usage:
|
||||
noperm(mypermstring)
|
||||
noperm(perm1, perm2, perm3, ...)
|
||||
|
||||
A negative permission; this will return False only if
|
||||
the checking object *has any* of the given permission(s), True
|
||||
otherwise. The searched permission cannot itself be a
|
||||
function-permission (i.e. you cannot wrap functions in
|
||||
functions).
|
||||
"""
|
||||
if not args:
|
||||
# this is an always-false permission
|
||||
return False
|
||||
return not has_perm_string(checking_obj, args)
|
||||
|
||||
def is_superuser(checking_obj, checked_obj, *args):
|
||||
"""
|
||||
Usage:
|
||||
is_superuser()
|
||||
|
||||
Determines if the checking object is superuser.
|
||||
"""
|
||||
if hasattr(checking_obj, 'is_superuser'):
|
||||
return checking_obj.is_superuser
|
||||
return False
|
||||
|
||||
def has_id(checking_obj, checked_obj, *args):
|
||||
"""
|
||||
Usage:
|
||||
has_id(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())
|
||||
except ValueError:
|
||||
return False
|
||||
if hasattr(checking_obj, 'id'):
|
||||
return dbref == checking_obj.id
|
||||
return False
|
||||
|
||||
def has_attr(checking_obj, checked_obj, *args):
|
||||
"""
|
||||
Usage:
|
||||
has_attr(attrname)
|
||||
has_attr(attrname, value)
|
||||
|
||||
Searches attributes *and* properties stored on the checking
|
||||
object. The first form works like a flag - if the attribute/property
|
||||
exists on the object, it returns True. 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()
|
||||
# first, look for normal properties on the object trying to gain access
|
||||
if hasattr(checking_obj, attrname):
|
||||
if value:
|
||||
return str(getattr(checking_obj, attrname)) == value
|
||||
return True
|
||||
# check attributes, if they exist
|
||||
#print "lockfunc default: %s (%s)" % (checking_obj, attrname)
|
||||
if hasattr(checking_obj, 'has_attribute') \
|
||||
and checking_obj.has_attribute(attrname):
|
||||
if value:
|
||||
return hasattr(checking_obj, 'attr') \
|
||||
and checking_obj.attr(attrname) == value
|
||||
return True
|
||||
return False
|
||||
|
||||
25
src/permissions/manager.py
Normal file
25
src/permissions/manager.py
Normal file
|
|
@ -0,0 +1,25 @@
|
|||
"""
|
||||
A simple manager for Permission groups
|
||||
"""
|
||||
|
||||
from django.db import models
|
||||
|
||||
class PermissionGroupManager(models.Manager):
|
||||
"Adds a search method to the default manager"
|
||||
|
||||
def search_permgroup(self, ostring):
|
||||
"""
|
||||
Find a permission group
|
||||
|
||||
ostring = permission group name (case sensitive)
|
||||
or database dbref
|
||||
"""
|
||||
groups = []
|
||||
try:
|
||||
dbref = int(ostring.strip('#'))
|
||||
groups = self.filter(id=dbref)
|
||||
except Exception:
|
||||
pass
|
||||
if not groups:
|
||||
groups = self.filter(db_key=ostring)
|
||||
return groups
|
||||
150
src/permissions/models.py
Normal file
150
src/permissions/models.py
Normal file
|
|
@ -0,0 +1,150 @@
|
|||
"""
|
||||
The PermissionGroup model clumps permissions together into
|
||||
manageable chunks.
|
||||
"""
|
||||
|
||||
from django.db import models
|
||||
from src.utils.idmapper.models import SharedMemoryModel
|
||||
from src.permissions.manager import PermissionGroupManager
|
||||
from src.utils.utils import is_iter
|
||||
|
||||
#------------------------------------------------------------
|
||||
#
|
||||
# PermissionGroup
|
||||
#
|
||||
#------------------------------------------------------------
|
||||
|
||||
class PermissionGroup(SharedMemoryModel):
|
||||
"""
|
||||
This groups permissions into a clump.
|
||||
|
||||
The following properties are defined:
|
||||
key - main ident for group
|
||||
desc - optional description of group
|
||||
group_permissions - the perms stored in group
|
||||
permissions - the group's own permissions
|
||||
|
||||
"""
|
||||
|
||||
#
|
||||
# PermissionGroup database model setup
|
||||
#
|
||||
# These database fields are all set using their corresponding properties,
|
||||
# named same as the field, but withtout the db_* prefix.
|
||||
#
|
||||
|
||||
# identifier for the group
|
||||
db_key = models.CharField(max_length=80, unique=True)
|
||||
# description of the group's permission contents
|
||||
db_desc = models.CharField(max_length=255, null=True, blank=True)
|
||||
# the permissions stored in this group; comma separated string
|
||||
# (NOT to be confused with the group object's own permissions!)
|
||||
db_group_permissions = models.TextField(blank=True)
|
||||
# OBS - this is the groups OWN permissions, for accessing/changing
|
||||
# the group object itself (comma-separated string)!
|
||||
db_permissions = models.CharField(max_length=256, blank=True)
|
||||
|
||||
# Database manager
|
||||
objects = PermissionGroupManager()
|
||||
|
||||
# Wrapper properties to easily set database fields. These are
|
||||
# @property decorators that allows to access these fields using
|
||||
# normal python operations (without having to remember to save()
|
||||
# etc). So e.g. a property 'attr' has a get/set/del decorator
|
||||
# defined that allows the user to do self.attr = value,
|
||||
# value = self.attr and del self.attr respectively (where self
|
||||
# is the object in question).
|
||||
|
||||
# key property (wraps db_key)
|
||||
#@property
|
||||
def key_get(self):
|
||||
"Getter. Allows for value = self.key"
|
||||
return self.db_key
|
||||
#@key.setter
|
||||
def key_set(self, value):
|
||||
"Setter. Allows for self.key = value"
|
||||
self.db_key = value
|
||||
self.save()
|
||||
#@key.deleter
|
||||
def key_del(self):
|
||||
"Deleter. Allows for del self.key"
|
||||
self.db_key = None
|
||||
self.save()
|
||||
key = property(key_get, key_set, key_del)
|
||||
|
||||
# desc property (wraps db_desc)
|
||||
#@property
|
||||
def desc_get(self):
|
||||
"Getter. Allows for value = self.desc"
|
||||
return self.db_desc
|
||||
#@desc.setter
|
||||
def desc_set(self, value):
|
||||
"Setter. Allows for self.desc = value"
|
||||
self.db_desc = value
|
||||
self.save()
|
||||
#@desc.deleter
|
||||
def desc_del(self):
|
||||
"Deleter. Allows for del self.desc"
|
||||
self.db_desc = None
|
||||
self.save()
|
||||
desc = property(desc_get, desc_set, desc_del)
|
||||
|
||||
# group_permissions property
|
||||
#@property
|
||||
def group_permissions_get(self):
|
||||
"Getter. Allows for value = self.name. Returns a list of group_permissions."
|
||||
if self.db_group_permissions:
|
||||
return [perm.strip() for perm in self.db_group_permissions.split(',')]
|
||||
return []
|
||||
#@group_permissions.setter
|
||||
def group_permissions_set(self, value):
|
||||
"Setter. Allows for self.name = value. Stores as a comma-separated string."
|
||||
if is_iter(value):
|
||||
value = ",".join([str(val).strip().lower() for val in value])
|
||||
self.db_group_permissions = value
|
||||
self.save()
|
||||
#@group_permissions.deleter
|
||||
def group_permissions_del(self):
|
||||
"Deleter. Allows for del self.name"
|
||||
self.db_group_permissions = ""
|
||||
self.save()
|
||||
group_permissions = property(group_permissions_get, group_permissions_set, group_permissions_del)
|
||||
|
||||
# permissions property
|
||||
#@property
|
||||
def permissions_get(self):
|
||||
"Getter. Allows for value = self.name. Returns a list of permissions."
|
||||
if self.db_permissions:
|
||||
return [perm.strip() for perm in self.db_permissions.split(',')]
|
||||
return []
|
||||
#@permissions.setter
|
||||
def permissions_set(self, value):
|
||||
"Setter. Allows for self.name = value. Stores as a comma-separated string."
|
||||
if is_iter(value):
|
||||
value = ",".join([str(val).strip().lower() for val in value])
|
||||
self.db_permissions = value
|
||||
self.save()
|
||||
#@permissions.deleter
|
||||
def permissions_del(self):
|
||||
"Deleter. Allows for del self.name"
|
||||
self.db_permissions = ""
|
||||
self.save()
|
||||
permissions = property(permissions_get, permissions_set, permissions_del)
|
||||
|
||||
|
||||
class Meta:
|
||||
"Define Django meta options"
|
||||
verbose_name = "Permission Group"
|
||||
verbose_name_plural = "Permission Groups"
|
||||
|
||||
#
|
||||
# PermissionGroup help method
|
||||
#
|
||||
#
|
||||
|
||||
def contains(self, permission):
|
||||
"""
|
||||
Checks if the given permission string is defined in the
|
||||
permission group.
|
||||
"""
|
||||
return permission in self.group_permissions
|
||||
644
src/permissions/permissions.py
Normal file
644
src/permissions/permissions.py
Normal file
|
|
@ -0,0 +1,644 @@
|
|||
"""
|
||||
Permissions
|
||||
|
||||
Evennia's permission system can be as coarse or fine-grained as desired.
|
||||
|
||||
**Note: Django's in-built permission checking is still used for web-related access
|
||||
to the admin site etc. This uses the normal django permission
|
||||
strings of the form 'appname.permstring' and automatically adds three of them
|
||||
for each model - for src/object this would be for example
|
||||
'object.create', 'object.admin' and 'object.edit'. Checking and permissions
|
||||
are done completely separate from the present mud permission system, see
|
||||
Django manuals. **
|
||||
|
||||
|
||||
|
||||
Almost all engine elements (Commands, Players, Objects, Scripts,
|
||||
Channels) have a field or variable called 'permissions' - this holds a
|
||||
list or a comma-separated string of 'permission strings'.
|
||||
|
||||
Whenever a permission is to be checked there is always an
|
||||
'Accessing object' and an 'Accessed object'. The 'permissions' field
|
||||
on the two objects are matched against each other. A permission field
|
||||
can contain one of two types of strings - 'keys' and 'locks' and the
|
||||
accessing object's keys are matched against the accessing object's locks
|
||||
depending on which type of access is required. The check function
|
||||
has_perm (in this module) is used to handle the check.
|
||||
|
||||
access_obj.permissions (keys) ---> | |
|
||||
lock_type ---> | has_perm() | ---> False/True
|
||||
accessed_obj.permissions (locks) ---> | |
|
||||
|
||||
If there are no locks (of the right type) available on accessed_obj, access is
|
||||
granted by default. You can however give an argument to has_perm() to reverse
|
||||
this policy to one where default is no access until explicitly granted.
|
||||
|
||||
|
||||
- Keys
|
||||
|
||||
Keys are simple strings that can contain any alphanumerical symbol and
|
||||
be of any length. They are case-sensitive. Only whitespace, colons(:) and
|
||||
commas (,) are not allowed in keys.
|
||||
|
||||
examples:
|
||||
myperm
|
||||
look_perm
|
||||
Builders
|
||||
obj.create (this is what a general Django key looks like)
|
||||
|
||||
|
||||
- Locks
|
||||
|
||||
A lock always consists of two parts separated by a colon (:). A lock must nowhere
|
||||
contain commas (,) since these are used as separators between different locks and keys
|
||||
during storage.
|
||||
|
||||
-type header- -lock body-
|
||||
type1 type2 FLAG:string1 string2 string3()
|
||||
|
||||
type header
|
||||
|
||||
The first part is the 'type' of lock, i.e. what kind of functionality
|
||||
this lock regulates. The function has_perm() takes this as an argument and uses it to sort
|
||||
out which locks are checked (if any).
|
||||
A type name is not case sensitive but may not contain any whitespace (or colons). Use
|
||||
whitespaces to expand the number of types this lock represents.
|
||||
The last word in the type header may be a special flag that regulates how the second
|
||||
part of the lock is handled, especially if the second part is a space-separated list
|
||||
of permissions:
|
||||
- OR or ANY (or none, default) - ANY one match in list means that the entire lock is passed.
|
||||
- AND or ALL - ALL permissions in list must be matched by accessing obj for lock to pass
|
||||
- NOT - inverses the lock. A matched lock means False and vice versa.
|
||||
|
||||
lock body
|
||||
|
||||
The second part, after the comma, is the function body of the lock. The function body
|
||||
can look in several ways:
|
||||
- it could be a permission string, to be matched directly with a key
|
||||
- it could be a function call to a function defined in a safe module (this returns True/False)
|
||||
- it could be a space-separated list of any combination of the above.
|
||||
- it could be one of the non-passable keywords FALSE, LOCK or IMPASSABLE (can be put in a
|
||||
list too, but the list will then always return False, so they are usually
|
||||
used on their own).
|
||||
|
||||
examples:
|
||||
|
||||
look:look_perm
|
||||
|
||||
this lock is of type 'look' and simply checks if the accessing_obj has
|
||||
they key look_perm or not.
|
||||
|
||||
add edit:Builders
|
||||
|
||||
this lock regulates two lock types, 'add' and 'edit'. In both cases,
|
||||
having the key 'Builders' will pass the lock.
|
||||
|
||||
add edit:Builders Admin
|
||||
|
||||
this is like above, but it is passed if the accessing object has either
|
||||
the key Builders or the key Admin (or both)
|
||||
|
||||
system NOT: Untrusted
|
||||
|
||||
this lock of type 'system' is passed if the accessing object does NOT have
|
||||
the key 'Untrusted', since the NOT flag inverses the lock result.
|
||||
|
||||
delete:FALSE
|
||||
|
||||
this lock, of type 'delete' can never be passed since the keyword FALSE
|
||||
is always False (same with LOCK and IMPASSABLE).
|
||||
|
||||
open:
|
||||
|
||||
this lock has an empty lock body and is an always passable lock. Unless you
|
||||
change how has_perm() behaves you can usually just completely skip defining
|
||||
this lock to get the same effect.
|
||||
|
||||
enter:has_key()
|
||||
|
||||
this 'enter'-type lock might for example be placed on a door. The lock body
|
||||
is a function has_key() defined in a module set in the settings. See below
|
||||
for more info on lock functions. It is up the admin to create
|
||||
these lock functions ahead of time as needed for the game.
|
||||
|
||||
unlock:has_pickpocket_lvl(4,difficulty=7)
|
||||
|
||||
function locks can even take arguments, in this case it calls a function
|
||||
has_pick() with info on how hard the lock is.
|
||||
|
||||
delete AND: Builders can_delete has_credits()
|
||||
|
||||
Since the keyword AND is set (ALL would also work), the
|
||||
accessing object must have both the 'Builders', 'can_delete'
|
||||
*and* the function lock 'has_credits()' in order to pass the lock.
|
||||
|
||||
- More on functional locks
|
||||
|
||||
A function lock goes into the second part of a lock definition (after the colon).
|
||||
|
||||
Syntax:
|
||||
funcname(arg3, arg4, ...)
|
||||
arg1 == always accessing_obj
|
||||
arg2 == always accessed_obj
|
||||
|
||||
A function lock is called just like any Python function and should be defined in
|
||||
the same way. The system searches for the first matching function in one of the
|
||||
modules defined by settings.PERMISSION_FUNC_MODULES and executes it.
|
||||
|
||||
Accessing_object and Accessed_object are always passed, followed by the arguments
|
||||
and keywords supplied in the permission. The function should return a boolean and
|
||||
must make sure to handle any possible type of objects being passed in
|
||||
the first two arguments (such as Command objects, Scripts or whatever). There is a
|
||||
convenience function available in this module (get_types) for doing just that.
|
||||
|
||||
examples:
|
||||
'is_stronger_than(40)'
|
||||
'is_suitably_dressed()'
|
||||
'has_lockpick_higher_than(24, difficulty=4)'
|
||||
'has_attr_value('mood', 'happy')
|
||||
|
||||
|
||||
- Permissions string
|
||||
|
||||
As mentioned, keys and locks are stored together on an object in a string
|
||||
called 'permissions'. You are most likely to come into contact with this
|
||||
when defining new Commands.
|
||||
|
||||
This is a comma-separated string (which is why commas
|
||||
are not allowed in keys nor in locks). Keys and Locks can appear in any
|
||||
order in the permission string, they are easily separated by the colon
|
||||
appearing only in locks.
|
||||
|
||||
example:
|
||||
|
||||
'call ALL:can_edit Builders'
|
||||
|
||||
this might be a Command permission and has only one lock on it (a command usually have no need
|
||||
for keys of its own) but two requirements to be able to call it: The accessing object
|
||||
(probably a player) must have both the can_edit and Builders keys (one is not enough
|
||||
since the ALL flag is set).
|
||||
|
||||
'Builders, edit_channels, control:has_id(45)'
|
||||
|
||||
this permission string could sit on a Character object. It has two keys and one lock.
|
||||
The keys tell us Character is a Builder and also has the edit_channels permission.
|
||||
The lock is checked when someone tries to gain 'control' over this object (whatever
|
||||
this means in your game). This lock calls the function has_id() with the argument 45.
|
||||
We can't see here what has_id() actually does, but a likely implementation would
|
||||
be to return True only if the accessing object actually has an id of 45.
|
||||
|
||||
|
||||
- Reserved Server lock types
|
||||
|
||||
The Evennia server (i.e. everything in /src) tries to 'outsource' as many permission checks
|
||||
as possible to what the admin can customize in /game. All the admin needs to do is import
|
||||
has_perm() from this module to use the system. As seen above, the actual syntax of
|
||||
locks and keys above gives the admin much freedom in designing their own system.
|
||||
|
||||
That said, there are a few actions that cannot be outsourced due to technical reasons.
|
||||
In these situations the server must hard-code its permission checks. What this means
|
||||
is that it always calls has_perm() with a specific lock-type keyword that you cannot change. For
|
||||
example it always checks if a command may be accessed by matching the calling player's
|
||||
keys against command-locks of type 'call'. Below you will find all hard-coded lock types
|
||||
the server checks and when it does it.
|
||||
|
||||
locktype entity situation
|
||||
|
||||
call Command when a player tries to call a
|
||||
command. Determines if the command is available
|
||||
at all. Also determines if command will be
|
||||
visible in help lists etc.
|
||||
|
||||
chan_send Channels
|
||||
chan_listen "
|
||||
|
||||
traverse Exits
|
||||
|
||||
|
||||
|
||||
"""
|
||||
|
||||
from django.conf import settings
|
||||
from src.permissions.models import PermissionGroup
|
||||
from src.utils import logger
|
||||
from src.utils.utils import is_iter
|
||||
|
||||
IMPASSABLE = ['FALSE', 'LOCK', 'IMPASSABLE']
|
||||
ORFLAGS = ['OR', 'ANY']
|
||||
ANDFLAGS = ['AND', 'ALL']
|
||||
NOTFLAGS = ['NOT']
|
||||
|
||||
LOCK_FUNC_PATHS = settings.PERMISSION_FUNC_MODULES
|
||||
CACHED_FUNC_MODULES = []
|
||||
|
||||
PARENTS = {
|
||||
"command":"src.commands.command.Command",
|
||||
"msg":"src.comms.models.Msg",
|
||||
"channel":"src.comms.models.Channel",
|
||||
"help":"src.help.models.HelpEntry",
|
||||
"typeclass":"src.typeclasses.typeclass.TypeClass",
|
||||
"script":"src.scripts.models.ScriptDB",
|
||||
"object":"src.objects.models.ObjectDB",
|
||||
"player":"src.players.models.Player"}
|
||||
|
||||
#
|
||||
# Utility functions for handling permissions
|
||||
#
|
||||
|
||||
def perm_to_list(permissions):
|
||||
"""
|
||||
Process a permstring and return a list
|
||||
|
||||
permissions - can be a list of permissions, a permission string
|
||||
or a comma-separated string of permissions.
|
||||
"""
|
||||
if not permissions:
|
||||
return []
|
||||
if not is_iter(permissions):
|
||||
# convert input to a list
|
||||
permissions = str(permissions)
|
||||
if ',' in permissions:
|
||||
permissions = permissions.split(',')
|
||||
else:
|
||||
permissions = [permissions]
|
||||
p_nested = []
|
||||
for nested_perm in (p for p in permissions if ',' in p):
|
||||
# handling eventual weird nested comma-separated
|
||||
# permissions inside lists
|
||||
p_nested.extend(nested_perm.split(','))
|
||||
permissions.extend(p_nested)
|
||||
|
||||
# merge units with unmatched parenthesis (so e.g. '(a,b,c,d)' are not
|
||||
# split by comma separation (this allows for functional locks with
|
||||
# multiple arguments, like 'has_attr(attrname, attrvalue)'). This
|
||||
# also removes all whitespace in the parentheses to avoid confusion later.
|
||||
|
||||
lparents = 0
|
||||
rparents = 0
|
||||
in_block = False
|
||||
perm_list = []
|
||||
for perm in permissions:
|
||||
lparents += perm.count('(')
|
||||
rparents += perm.count(')')
|
||||
if lparents > rparents:
|
||||
# unmatched left-parenthesis - trigger block preservation
|
||||
if not in_block:
|
||||
# beginning of block
|
||||
in_block = True
|
||||
perm_list.append(perm)
|
||||
else:
|
||||
# in block
|
||||
block = perm_list.pop()
|
||||
perm_list.append(",".join([block.strip(), perm.strip()]))
|
||||
elif in_block:
|
||||
# parentheses match again - end the block
|
||||
in_block = False
|
||||
block = perm_list.pop()
|
||||
perm_list.append(",".join([block.strip(), perm.strip()]))
|
||||
else:
|
||||
# no parenthesis/block
|
||||
perm_list.append(perm.strip())
|
||||
return perm_list
|
||||
|
||||
def set_perm(obj, new_perms, replace=True):
|
||||
"""
|
||||
Set a permission string on an object.
|
||||
The permissions given by default replace the old one.
|
||||
If 'replace' is unset, the new one will be appended to
|
||||
the old ones.
|
||||
|
||||
obj - object to receive permission. must have field/variable
|
||||
named 'permissions' for this to work.
|
||||
new_perms - a permission string, a list of permission strings
|
||||
or a comma-separated string of permissions
|
||||
replace - clear and replace old permission completely
|
||||
|
||||
Note - this is also how you add an entity to a group
|
||||
"""
|
||||
try:
|
||||
# get the old permissions if possible
|
||||
obj_perms = perm_to_list(obj.permissions)
|
||||
except AttributeError:
|
||||
# this object cannot get a permission
|
||||
return False
|
||||
new_perms = perm_to_list(new_perms)
|
||||
if replace:
|
||||
# replace permissions completely
|
||||
obj_perms = new_perms
|
||||
else:
|
||||
# extend the old permissions with the new ones
|
||||
for newperm in (perm for perm in new_perms if perm not in obj_perms):
|
||||
obj_perms.append(newperm)
|
||||
# set on object as comma-separated list.
|
||||
obj.permissions = ",".join(str(perm).strip() for perm in obj_perms)
|
||||
try:
|
||||
# E.g. Commands are not db-connected and cannot save,
|
||||
# so we ignore errors here.
|
||||
obj.save()
|
||||
except Exception:
|
||||
pass
|
||||
return True
|
||||
|
||||
def add_perm(obj, add_perms):
|
||||
"""
|
||||
Convenience function to add a permission to an entity.
|
||||
"""
|
||||
return set_perm(obj, add_perms, replace=False)
|
||||
|
||||
def del_perm(obj, del_perms):
|
||||
"""
|
||||
Remove permission from object (if possible)
|
||||
"""
|
||||
try:
|
||||
obj_perms = perm_to_list(obj.permissions)
|
||||
except AttributeError:
|
||||
return False
|
||||
del_perms = perm_to_list(del_perms)
|
||||
obj_perms = [perm for perm in obj_perms if perm not in del_perms]
|
||||
obj.permissions = ",".join(str(perm) for perm in obj_perms)
|
||||
try:
|
||||
obj.save()
|
||||
except Exception:
|
||||
pass
|
||||
return True
|
||||
|
||||
def get_types(accessing_obj, accessed_obj):
|
||||
"""
|
||||
A convenience function for determining what type the objects are.
|
||||
This is intended for easy importing into the modules
|
||||
defined in LOCK_FUNC_PATHS.
|
||||
"""
|
||||
|
||||
def has_parent(basepath, obj):
|
||||
"Checks if basepath is somewhere is objs parent tree."
|
||||
return any(cls for cls in obj.__class__.mro()
|
||||
if basepath == "%s.%s" % (cls.__module__, cls.__name__))
|
||||
|
||||
checking_type = None
|
||||
checked_type = None
|
||||
try:
|
||||
checking_type = [parent for parent, path in PARENTS.items()
|
||||
if has_parent(path, accessing_obj)][0]
|
||||
if checking_type == 'typeclass':
|
||||
checking_type = [parent for parent, path in PARENTS.items()
|
||||
if has_parent(path, accessing_obj.db)][0]
|
||||
except IndexError:
|
||||
logger.log_trace("LockFunc: %s is not of a valid type."
|
||||
% accessing_obj)
|
||||
raise
|
||||
try:
|
||||
checked_type = [parent for parent, path in PARENTS.items()
|
||||
if has_parent(path, accessed_obj)][0]
|
||||
if checking_type == 'typeclass':
|
||||
checked_type = [parent for parent, path in PARENTS.items()
|
||||
if has_parent(path, accessed_obj.db)][0]
|
||||
except IndexError:
|
||||
logger.log_trace("LockFunc: %s is not of a valid type."
|
||||
% accessed_obj)
|
||||
raise
|
||||
return (checking_type, checked_type)
|
||||
|
||||
|
||||
#
|
||||
# helper functions for has_perm()
|
||||
#
|
||||
|
||||
|
||||
def append_group_permissions(keylist):
|
||||
"""
|
||||
Step through keylist and discover if
|
||||
one the keys match a permission group name
|
||||
(case sensitive). In that case, go into
|
||||
that group and add its permissions to
|
||||
the keylist.
|
||||
"""
|
||||
groups = []
|
||||
for match_key in keylist:
|
||||
try:
|
||||
groups.append(PermissionGroup.objects.get(db_key=match_key))
|
||||
except Exception:
|
||||
pass
|
||||
for group in groups:
|
||||
keylist.extend(perm_to_list(group.group_permissions))
|
||||
return list(set(keylist)) # set makes elements of keylist unique
|
||||
|
||||
def try_impassable(lockdefs):
|
||||
"""
|
||||
Test if this list of lockdefs is impassable.
|
||||
"""
|
||||
return any(True for lockdef in lockdefs if lockdef in IMPASSABLE)
|
||||
|
||||
def try_key_lock(iflag, keylist, lockdefs):
|
||||
"""
|
||||
Test a direct-comparison match between keys and lockstrings.
|
||||
Returns the number of matches found.
|
||||
"""
|
||||
if iflag in ANDFLAGS:
|
||||
return len([True for key in keylist if key in lockdefs])
|
||||
elif iflag in NOTFLAGS:
|
||||
return not any(True for key in keylist if key in lockdefs)
|
||||
else:
|
||||
return any(True for key in keylist if key in lockdefs)
|
||||
|
||||
def try_functional_lock(lflag, lockdefs, accessing_obj, accessed_obj):
|
||||
"""
|
||||
Functional lock
|
||||
|
||||
lockdefs is a list of permission strings (called by check_lock)
|
||||
"""
|
||||
global CACHED_FUNC_MODULES
|
||||
|
||||
if not CACHED_FUNC_MODULES:
|
||||
# Cache the imported func module(s) once and for all
|
||||
# so we don't have to re-import them for every call.
|
||||
# We allow for LOCK_FUNC_PATHS to be a tuple of
|
||||
# paths as well.
|
||||
CACHED_FUNC_MODULES = []
|
||||
try:
|
||||
module_paths = list(LOCK_FUNC_PATHS)
|
||||
except Exception:
|
||||
module_paths = [LOCK_FUNC_PATHS]
|
||||
for path in module_paths:
|
||||
try:
|
||||
CACHED_FUNC_MODULES.append(__import__(path, fromlist=[True]))
|
||||
except ImportError:
|
||||
logger.log_trace("lock func: import error for '%s'" % path)
|
||||
continue
|
||||
|
||||
# try to execute functions, if they exist
|
||||
#print "locklist:", locklist
|
||||
passed_locks = 0
|
||||
for lockstring in lockdefs:
|
||||
if not lockstring \
|
||||
or not ('(' in lockstring and ')' in lockstring) \
|
||||
or not (lockstring.find('(') < lockstring.find(')')):
|
||||
# must be a valid function() call
|
||||
continue
|
||||
funcname, args = (str(part).strip() for part in lockstring.split('(', 1))
|
||||
args = args.rstrip(')').split(',')
|
||||
kwargs = [kwarg for kwarg in args if '=' in kwarg]
|
||||
args = tuple([arg for arg in args if arg not in kwargs])
|
||||
kwargs = dict([(key.strip(), value.strip()) for key, value in [kwarg.split('=', 1) for kwarg in kwargs]])
|
||||
#print "%s '%s'" % (funcname, args)
|
||||
for module in CACHED_FUNC_MODULES:
|
||||
# step through all safe modules, executing the first one that matches
|
||||
lockfunc = module.__dict__.get(funcname, None)
|
||||
if callable(lockfunc):
|
||||
try:
|
||||
# try the lockfunction.
|
||||
if lockfunc(accessing_obj, accessed_obj, *args, **kwargs):
|
||||
if lflag in ANDFLAGS:
|
||||
passed_locks += 1
|
||||
elif lflag in NOTFLAGS:
|
||||
return False
|
||||
else:
|
||||
return True
|
||||
except Exception:
|
||||
continue
|
||||
return passed_locks
|
||||
|
||||
|
||||
#------------------------------------------------------------
|
||||
# has_perm & has_perm_string : main access functions
|
||||
#------------------------------------------------------------
|
||||
|
||||
def has_perm(accessing_obj, accessed_obj, lock_type, default_deny=False):
|
||||
"""
|
||||
The main access method of the permission system. Note that
|
||||
this will not perform checks against django's in-built permission
|
||||
system, that is assumed to be done in the calling function
|
||||
after this method returns False.
|
||||
|
||||
accessing_obj - the object trying to gain access
|
||||
accessed_obj - the object being checked for access
|
||||
lock_type - Only locks of this type 'lock_type:permissionstring'
|
||||
will be considered for a match.
|
||||
default_deny - Normally, if no suitable locks are found on the object, access
|
||||
is granted by default. This switch changes security policy to
|
||||
instead lock down the object unless access is explicitly given.
|
||||
"""
|
||||
|
||||
# Get the list of locks of the accessed_object
|
||||
|
||||
try:
|
||||
locklist = [lock for lock in perm_to_list(accessed_obj.permissions) if ':' in lock]
|
||||
except AttributeError:
|
||||
# This is not a valid permissable object at all
|
||||
logger.log_trace()
|
||||
return False
|
||||
|
||||
# filter the locks to find only those of the correct lock_type. This creates
|
||||
# a list with elements being tuples (flag, [lockdef, lockdef, ...])
|
||||
|
||||
lock_type = lock_type.lower()
|
||||
locklist = [(typelist[-1].strip(), [lo.strip() for lo in lock.split(None)])
|
||||
for typelist, lock in [(ltype.split(None), lock)
|
||||
for ltype, lock in [lock.split(':', 1)
|
||||
for lock in locklist]]
|
||||
if typelist and lock_type in typelist]
|
||||
|
||||
if not locklist or not any(locklist):
|
||||
# No locks; use default security policy
|
||||
return not default_deny
|
||||
|
||||
# we have locks of the right type. Set default flag OR on all that
|
||||
# don't explictly specify a flag (AND, OR, NOT). These flags determine
|
||||
# how the permstrings in the lock definition should relate to one another.
|
||||
locktemp = []
|
||||
for ltype, lock in locklist:
|
||||
if ltype not in ANDFLAGS and ltype not in NOTFLAGS:
|
||||
ltype = 'OR'
|
||||
locktemp.append((ltype, lock))
|
||||
locklist = locktemp
|
||||
|
||||
# Get the list of keys of the accessing_object
|
||||
|
||||
keylist = []
|
||||
|
||||
#print "testing %s" % accessing_obj.__class__
|
||||
if hasattr(accessing_obj, 'is_superuser') and accessing_obj.is_superuser:
|
||||
# superusers always have access.
|
||||
return True
|
||||
|
||||
# try to add permissions from connected player
|
||||
if hasattr(accessing_obj, 'has_player') and accessing_obj.has_player:
|
||||
# accessing_obj has a valid, connected player. We start with
|
||||
# those permissions.
|
||||
player = accessing_obj.player
|
||||
if player.is_superuser:
|
||||
# superuser always has access
|
||||
return True
|
||||
try:
|
||||
keylist.extend([perm for perm in perm_to_list(player.permissions)
|
||||
if not ':' in perm])
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
# next we get the permissions directly from accessing_obj.
|
||||
try:
|
||||
keylist.extend([perm for perm in perm_to_list(accessing_obj.permissions)
|
||||
if not ':' in perm])
|
||||
except Exception:
|
||||
# not a valid permissable object
|
||||
return False
|
||||
# expand also with group permissions, if any
|
||||
keylist = append_group_permissions(keylist)
|
||||
|
||||
#print "keylist: %s" % keylist
|
||||
|
||||
|
||||
# Test permissions against the locks
|
||||
|
||||
|
||||
for lflag, lockdefs in locklist:
|
||||
|
||||
# impassable locks normally shuts down the entire operation right away.
|
||||
if try_impassable(lockdefs):
|
||||
return lflag in NOTFLAGS
|
||||
|
||||
# test direct key-lock comparison and functional locks
|
||||
if lflag in ANDFLAGS:
|
||||
# with the AND flag, we have to match all lockdefs to pass the lock
|
||||
passed_locks = try_key_lock(lflag, keylist, lockdefs)
|
||||
passed_locks += try_functional_lock(lflag, lockdefs, accessing_obj, accessed_obj)
|
||||
if passed_locks == len(lockdefs):
|
||||
return True
|
||||
else:
|
||||
# normal operation; any match passes the lock
|
||||
if try_key_lock(lflag, keylist, lockdefs):
|
||||
return True
|
||||
if try_functional_lock(lflag, lockdefs, accessing_obj, accessed_obj):
|
||||
return True
|
||||
# If we fall through to here, we don't have access
|
||||
return False
|
||||
|
||||
def has_perm_string(accessing_obj, lock_list, default_deny=False):
|
||||
"""
|
||||
This tests the given accessing object against the
|
||||
given string rather than a particular accessing object.
|
||||
|
||||
OBS: Be careful if supplying function locks to this method since
|
||||
there is no meaningful accessed_obj present (the one fed to
|
||||
the function is just a dummy).
|
||||
|
||||
accessing_obj - the object being checked for permissions
|
||||
lock_list - a list or a comma-separated string of lock definitions.
|
||||
default_deny - Normally, if no suitable locks are found on the object, access
|
||||
is granted by default. This switch changes security policy to
|
||||
instead lock down the object unless access is explicitly given.
|
||||
"""
|
||||
|
||||
# prepare the permissions we want, so it's properly stripped etc.
|
||||
class Dummy(object):
|
||||
"Dummy object"
|
||||
def __init__(self, permissions):
|
||||
self.permissions = permissions
|
||||
|
||||
if not is_iter(lock_list):
|
||||
lock_list = [perm for perm in lock_list.split(',')]
|
||||
lockstring = ",".join(["dummy:%s" % perm.strip() for perm in lock_list if perm.strip()])
|
||||
|
||||
# prepare a dummy object with the permissions
|
||||
accessed_obj = Dummy(lockstring)
|
||||
# call has_perm normally with the dummy object.
|
||||
return has_perm(accessing_obj, accessed_obj, 'dummy', default_deny)
|
||||
Loading…
Add table
Add a link
Reference in a new issue