Update Lock documentation. Update CHANGELOG

This commit is contained in:
Griatch 2023-12-02 19:19:29 +01:00
parent df877c6b06
commit 3add3168ea
5 changed files with 76 additions and 132 deletions

View file

@ -27,6 +27,11 @@
into services for easier overriding (Volund)
- [Feature][issue3307]: Add support for Attribute-categories when using the monitorhandler
with input funcs to monitor Attribute changes.
- [Feature][pull3342]: Add `Command.cmdset_source`, referring to the cmdset each
command was originally pulled from (Volund)
- [Feature][pull3343]: Add `access_type` as optional kwarg to lockfuncs (Volund)
- [Fix] (Backwards incompatible): Change `settings._TEST_ENVIRONMENT` to
`settings.TEST_ENVIRONMENT` to address issues during refactored startup sequence.
- [Fix][pull3197]: Make sure Global scripts only start in one place,
- [Fix][pull3324]: Make account-post-login-fail signal fire properly. Add
`CUSTOM_SIGNAL` for adding one's own signals (Volund)
@ -39,6 +44,7 @@
instantiated script. (Volund)
- [Fix][pull3338]: Resolve if/elif bug in XYZGrid contrib launch command
(jaborsh)
- [fix][issue3331]: Made XYZGrid query zcoords in a case-insensitive manner.
- [Fix][pull3322]: Fix `BaseOption.display` to always return a string.
- Docs: Lots of Typo fixes (iLPdev, InspectorCaracal, jaborsh)
- Beginner tutorial: Cleanup and starting earlier with explaining how to add to
@ -60,9 +66,12 @@
[pull3319]: https://github.com/evennia/evennia/pull/3319
[pull3324]: https://github.com/evennia/evennia/pull/3324
[pull3338]: https://github.com/evennia/evennia/pull/3338
[pull3342]: https://github.com/evennia/evennia/pull/3342
[pull3343]: https://github.com/evennia/evennia/pull/3343
[issue3272]: https://github.com/evennia/evennia/issues/3272
[issue3273]: https://github.com/evennia/evennia/issues/3273
[issue3308]: https://github.com/evennia/evennia/issues/3307
[issue3331]: https://github.com/evennia/evennia/issues/3331
## Evennia 2.3.0

View file

@ -1,19 +1,12 @@
# Locks
For most games it is a good idea to restrict what people can do. In Evennia such restrictions are
applied and checked by something called *locks*. All Evennia entities ([Commands](./Commands.md), [Objects](./Objects.md), [Scripts](./Scripts.md), [Accounts](./Accounts.md), [Help System](./Help-System.md), [messages](./Msg.md) and [channels](./Channels.md)) are accessed through locks.
For most games it is a good idea to restrict what people can do. In Evennia such restrictions are applied and checked by something called *locks*. All Evennia entities ([Commands](./Commands.md), [Objects](./Objects.md), [Scripts](./Scripts.md), [Accounts](./Accounts.md), [Help System](./Help-System.md), [messages](./Msg.md) and [channels](./Channels.md)) are accessed through locks.
A lock can be thought of as an "access rule" restricting a particular use of an Evennia entity.
Whenever another entity wants that kind of access the lock will analyze that entity in different
ways to determine if access should be granted or not. Evennia implements a "lockdown" philosophy - all entities are inaccessible unless you explicitly define a lock that allows some or full access.
Whenever another entity wants that kind of access the lock will analyze that entity in different ways to determine if access should be granted or not. Evennia implements a "lockdown" philosophy - all entities are inaccessible unless you explicitly define a lock that allows some or full access.
Let's take an example: An object has a lock on itself that restricts how people may "delete" that
object. Apart from knowing that it restricts deletion, the lock also knows that only players with
the specific ID of, say, `34` are allowed to delete it. So whenever a player tries to run `delete`
on the object, the `delete` command makes sure to check if this player is really allowed to do so.
It calls the lock, which in turn checks if the player's id is `34`. Only then will it allow `delete`
to go on with its job.
Let's take an example: An object has a lock on itself that restricts how people may "delete" that object. Apart from knowing that it restricts deletion, the lock also knows that only players with the specific ID of, say, `34` are allowed to delete it. So whenever a player tries to run `delete` on the object, the `delete` command makes sure to check if this player is really allowed to do so. It calls the lock, which in turn checks if the player's id is `34`. Only then will it allow `delete` to go on with its job.
## Working with locks
@ -21,21 +14,15 @@ The in-game command for setting locks on objects is `lock`:
> lock obj = <lockstring>
The `<lockstring>` is a string of a certain form that defines the behaviour of the lock. We will go
into more detail on how `<lockstring>` should look in the next section.
The `<lockstring>` is a string of a certain form that defines the behaviour of the lock. We will go into more detail on how `<lockstring>` should look in the next section.
Code-wise, Evennia handles locks through what is usually called `locks` on all relevant entities.
This is a handler that allows you to add, delete and check locks.
Code-wise, Evennia handles locks through what is usually called `locks` on all relevant entities. This is a handler that allows you to add, delete and check locks.
```python
myobj.locks.add(<lockstring>)
```
One can call `locks.check()` to perform a lock check, but to hide the underlying implementation all
objects also have a convenience function called `access`. This should preferably be used. In the
example below, `accessing_obj` is the object requesting the 'delete' access whereas `obj` is the
object that might get deleted. This is how it would look (and does look) from inside the `delete`
command:
One can call `locks.check()` to perform a lock check, but to hide the underlying implementation all objects also have a convenience function called `access`. This should preferably be used. In the example below, `accessing_obj` is the object requesting the 'delete' access whereas `obj` is the object that might get deleted. This is how it would look (and does look) from inside the `delete` command:
```python
if not obj.access(accessing_obj, 'delete'):
@ -63,29 +50,20 @@ Formally, a lockstring has the following syntax:
access_type: [NOT] lockfunc1([arg1,..]) [AND|OR] [NOT] lockfunc2([arg1,...]) [...]
```
where `[]` marks optional parts. `AND`, `OR` and `NOT` are not case sensitive and excess spaces are
ignored. `lockfunc1, lockfunc2` etc are special _lock functions_ available to the lock system.
where `[]` marks optional parts. `AND`, `OR` and `NOT` are not case sensitive and excess spaces are ignored. `lockfunc1, lockfunc2` etc are special _lock functions_ available to the lock system.
So, a lockstring consists of the type of restriction (the `access_type`), a colon (`:`) and then an
expression involving function calls that determine what is needed to pass the lock. Each function
returns either `True` or `False`. `AND`, `OR` and `NOT` work as they do normally in Python. If the
total result is `True`, the lock is passed.
So, a lockstring consists of the type of restriction (the `access_type`), a colon (`:`) and then an expression involving function calls that determine what is needed to pass the lock. Each function returns either `True` or `False`. `AND`, `OR` and `NOT` work as they do normally in Python. If the total result is `True`, the lock is passed.
You can create several lock types one after the other by separating them with a semicolon (`;`) in
the lockstring. The string below yields the same result as the previous example:
You can create several lock types one after the other by separating them with a semicolon (`;`) in the lockstring. The string below yields the same result as the previous example:
delete:id(34);edit:all();get: not attr(very_weak) or perm(Admin)
### Valid access_types
An `access_type`, the first part of a lockstring, defines what kind of capability a lock controls,
such as "delete" or "edit". You may in principle name your `access_type` anything as long as it is
unique for the particular object. The name of the access types is not case-sensitive.
An `access_type`, the first part of a lockstring, defines what kind of capability a lock controls, such as "delete" or "edit". You may in principle name your `access_type` anything as long as it is unique for the particular object. The name of the access types is not case-sensitive.
If you want to make sure the lock is used however, you should pick `access_type` names that you (or
the default command set) actually checks for, as in the example of `delete` above that uses the
'delete' `access_type`.
If you want to make sure the lock is used however, you should pick `access_type` names that you (or the default command set) actually checks for, as in the example of `delete` above that uses the 'delete' `access_type`.
Below are the access_types checked by the default commandset.
@ -97,16 +75,8 @@ Below are the access_types checked by the default commandset.
- `examine` - who may examine this object's properties.
- `delete` - who may delete the object.
- `edit` - who may edit properties and attributes of the object.
- `view` - if the `look` command will display/list this object in descriptions
and if you will be able to see its description. Note that if
you target it specifically by name, the system will still find it, just
not be able to look at it. See `search` lock to completely hide the item.
- `search` - this controls if the object can be found with the
`DefaultObject.search` method (usually referred to with `caller.search`
in Commands). This is how to create entirely 'undetectable' in-game objects.
If not setting this lock explicitly, all objects are assumed searchable.
Note that if you are aiming to make some _permanently invisible game system,
using a [Script](./Scripts.md) is a better bet.
- `view` - if the `look` command will display/list this object in descriptions and if you will be able to see its description. Note that if you target it specifically by name, the system will still find it, just not be able to look at it. See `search` lock to completely hide the item.
- `search` - this controls if the object can be found with the `DefaultObject.search` method (usually referred to with `caller.search` in Commands). This is how to create entirely 'undetectable' in-game objects. If not setting this lock explicitly, all objects are assumed searchable.
- `get`- who may pick up the object and carry it around.
- `puppet` - who may "become" this object and control it as their "character".
- `attrcreate` - who may create new attributes on the object (default True)
@ -125,8 +95,7 @@ Below are the access_types checked by the default commandset.
- `attrread` - see/access attribute
- `attredit` - change/delete attribute
- [Channels](./Channels.md):
- `control` - who is administrating the channel. This means the ability to delete the channel,
boot listeners etc.
- `control` - who is administrating the channel. This means the ability to delete the channel, boot listeners etc.
- `send` - who may send to the channel.
- `listen` - who may subscribe and listen to the channel.
- [HelpEntry](./Help-System.md):
@ -134,23 +103,17 @@ boot listeners etc.
- `edit` - who may edit this help entry.
So to take an example, whenever an exit is to be traversed, a lock of the type *traverse* will be checked. Defining a suitable lock type for an exit object would thus involve a lockstring `traverse: <lock functions>`.
### Custom access_types
As stated above, the `access_type` part of the lock is simply the 'name' or 'type' of the lock. The text is an arbitrary string that must be unique for an object. If adding a lock with the same `access_type` as one that already exists on the object, the new one override the old one.
For example, if you wanted to create a bulletin board system and wanted to restrict who can either
read a board or post to a board. You could then define locks such as:
For example, if you wanted to create a bulletin board system and wanted to restrict who can either read a board or post to a board. You could then define locks such as:
```python
obj.locks.add("read:perm(Player);post:perm(Admin)")
```
This will create a 'read' access type for Characters having the `Player` permission or above and a
'post' access type for those with `Admin` permissions or above (see below how the `perm()` lock
function works). When it comes time to test these permissions, simply check like this (in this
example, the `obj` may be a board on the bulletin board system and `accessing_obj` is the player
trying to read the board):
This will create a 'read' access type for Characters having the `Player` permission or above and a 'post' access type for those with `Admin` permissions or above (see below how the `perm()` lock function works). When it comes time to test these permissions, simply check like this (in this example, the `obj` may be a board on the bulletin board system and `accessing_obj` is the player trying to read the board):
```python
if not obj.access(accessing_obj, 'read'):
@ -160,27 +123,31 @@ trying to read the board):
### Lock functions
A _lock function_ is a normal Python function put in a place Evennia looks for such functions. The
modules Evennia looks at is the list `settings.LOCK_FUNC_MODULES`. *All functions* in any of those
modules will automatically be considered a valid lock function. The default ones are found in
`evennia/locks/lockfuncs.py` and you can start adding your own in `mygame/server/conf/lockfuncs.py`.
You can append the setting to add more module paths. To replace a default lock function, just add
your own with the same name.
A _lock function_ is a normal Python function put in a place Evennia looks for such functions. The modules Evennia looks at is the list `settings.LOCK_FUNC_MODULES`. *All functions* in any of those modules will automatically be considered a valid lock function. The default ones are found in `evennia/locks/lockfuncs.py` and you can start adding your own in `mygame/server/conf/lockfuncs.py`. You can append the setting to add more module paths. To replace a default lock function, just add your own with the same name.
A lock function must always accept at least two arguments - the *accessing object* (this is the
object wanting to get access) and the *accessed object* (this is the object with the lock). Those
two are fed automatically as the first two arguments to the function when the lock is checked. Any
arguments explicitly given in the lock definition will appear as extra arguments.
This is the basic definition of a lock function:
```python
def lockfunc_name(accessing_obj, accessed_obj, *args, **kwargs):
return True # or False
```
The `accessing object` is the object wanting to get access. The `accessed object` is the object being accessed (the object with the lock). The function always return a boolean determining if the lock is passed or not.
The `*args` will become the tuple of arguments given to the lockfunc. So for a lockstring `"edit:id(3)"` (a lockfunc named `id`), `*args` in the lockfunc would be `(3,)` .
The `**kwargs` dict has one default keyword always provided by Evennia, the `access_type`, which is a string with the access type being checked for. For the lockstring `"edit:id(3)"`, `access_type"` would be `"edit"`. This is unused by default Evennia.
Any arguments explicitly given in the lock definition will appear as extra arguments.
```python
# A simple example lock function. Called with e.g. `id(34)`. This is
# defined in, say mygame/server/conf/lockfuncs.py
# A simple example lock function. Called with e.g. `id(34)`. This is
# defined in, say mygame/server/conf/lockfuncs.py
def id(accessing_obj, accessed_obj, *args, **kwargs):
if args:
wanted_id = args[0]
return accessing_obj.id == wanted_id
return False
def id(accessing_obj, accessed_obj, *args, **kwargs):
if args:
wanted_id = args[0]
return accessing_obj.id == wanted_id
return False
```
The above could for example be used in a lock function like this:
@ -201,38 +168,27 @@ We could check if the "edit" lock is passed with something like this:
In this example, everyone except the `caller` with the right `id` will get the error.
> (Using the `*` and `**` syntax causes Python to magically put all extra arguments into a list
`args` and all keyword arguments into a dictionary `kwargs` respectively. If you are unfamiliar with
how `*args` and `**kwargs` work, see the Python manuals).
> (Using the `*` and `**` syntax causes Python to magically put all extra arguments into a list `args` and all keyword arguments into a dictionary `kwargs` respectively. If you are unfamiliar with how `*args` and `**kwargs` work, see the Python manuals).
Some useful default lockfuncs (see `src/locks/lockfuncs.py` for more):
- `true()/all()` - give access to everyone
- `false()/none()/superuser()` - give access to none. Superusers bypass the check entirely and are
thus the only ones who will pass this check.
- `perm(perm)` - this tries to match a given `permission` property, on an Account firsthand, on a
Character second. See [below](./Permissions.md).
- `false()/none()/superuser()` - give access to none. Superusers bypass the check entirely and are thus the only ones who will pass this check.
- `perm(perm)` - this tries to match a given `permission` property, on an Account firsthand, on a Character second. See [below](./Permissions.md).
- `perm_above(perm)` - like `perm` but requires a "higher" permission level than the one given.
- `id(num)/dbref(num)` - checks so the access_object has a certain dbref/id.
- `attr(attrname)` - checks if a certain [Attribute](./Attributes.md) exists on accessing_object.
- `attr(attrname, value)` - checks so an attribute exists on accessing_object *and* has the given
value.
- `attr_gt(attrname, value)` - checks so accessing_object has a value larger (`>`) than the given
value.
- `attr(attrname, value)` - checks so an attribute exists on accessing_object *and* has the given value.
- `attr_gt(attrname, value)` - checks so accessing_object has a value larger (`>`) than the given value.
- `attr_ge, attr_lt, attr_le, attr_ne` - corresponding for `>=`, `<`, `<=` and `!=`.
- `holds(objid)` - checks so the accessing objects contains an object of given name or dbref.
- `inside()` - checks so the accessing object is inside the accessed object (the inverse of
`holds()`).
- `pperm(perm)`, `pid(num)/pdbref(num)` - same as `perm`, `id/dbref` but always looks for
permissions and dbrefs of *Accounts*, not on Characters.
- `serversetting(settingname, value)` - Only returns True if Evennia has a given setting or a
setting set to a given value.
- `inside()` - checks so the accessing object is inside the accessed object (the inverse of `holds()`).
- `pperm(perm)`, `pid(num)/pdbref(num)` - same as `perm`, `id/dbref` but always looks for permissions and dbrefs of *Accounts*, not on Characters.
- `serversetting(settingname, value)` - Only returns True if Evennia has a given setting or a setting set to a given value.
### Checking simple strings
Sometimes you don't really need to look up a certain lock, you just want to check a lockstring. A
common use is inside Commands, in order to check if a user has a certain permission. The lockhandler
has a method `check_lockstring(accessing_obj, lockstring, bypass_superuser=False)` that allows this.
Sometimes you don't really need to look up a certain lock, you just want to check a lockstring. A common use is inside Commands, in order to check if a user has a certain permission. The lockhandler has a method `check_lockstring(accessing_obj, lockstring, bypass_superuser=False)` that allows this.
```python
# inside command definition
@ -245,42 +201,26 @@ Note here that the `access_type` can be left to a dummy value since this method
### Default locks
Evennia sets up a few basic locks on all new objects and accounts (if we didn't, noone would have
any access to anything from the start). This is all defined in the root [Typeclasses](./Typeclasses.md)
of the respective entity, in the hook method `basetype_setup()` (which you usually don't want to
edit unless you want to change how basic stuff like rooms and exits store their internal variables).
This is called once, before `at_object_creation`, so just put them in the latter method on your
child object to change the default. Also creation commands like `create` changes the locks of
objects you create - for example it sets the `control` lock_type so as to allow you, its creator, to
control and delete the object.
Evennia sets up a few basic locks on all new objects and accounts (if we didn't, noone would have any access to anything from the start). This is all defined in the root [Typeclasses](./Typeclasses.md) of the respective entity, in the hook method `basetype_setup()` (which you usually don't want to edit unless you want to change how basic stuff like rooms and exits store their internal variables). This is called once, before `at_object_creation`, so just put them in the latter method on your child object to change the default. Also creation commands like `create` changes the locks of objects you create - for example it sets the `control` lock_type so as to allow you, its creator, to control and delete the object.
## More Lock definition examples
examine: attr(eyesight, excellent) or perm(Builders)
You are only allowed to do *examine* on this object if you have 'excellent' eyesight (that is, has
an Attribute `eyesight` with the value `excellent` defined on yourself) or if you have the
"Builders" permission string assigned to you.
You are only allowed to do *examine* on this object if you have 'excellent' eyesight (that is, has an Attribute `eyesight` with the value `excellent` defined on yourself) or if you have the "Builders" permission string assigned to you.
open: holds('the green key') or perm(Builder)
This could be called by the `open` command on a "door" object. The check is passed if you are a
Builder or has the right key in your inventory.
This could be called by the `open` command on a "door" object. The check is passed if you are a Builder or has the right key in your inventory.
cmd: perm(Builders)
Evennia's command handler looks for a lock of type `cmd` to determine if a user is allowed to even
call upon a particular command or not. When you define a command, this is the kind of lock you must
set. See the default command set for lots of examples. If a character/account don't pass the `cmd`
lock type the command will not even appear in their `help` list.
Evennia's command handler looks for a lock of type `cmd` to determine if a user is allowed to even call upon a particular command or not. When you define a command, this is the kind of lock you must set. See the default command set for lots of examples. If a character/account don't pass the `cmd` lock type the command will not even appear in their `help` list.
cmd: not perm(no_tell)
"Permissions" can also be used to block users or implement highly specific bans. The above example
would be be added as a lock string to the `tell` command. This will allow everyone *not* having the
"permission" `no_tell` to use the `tell` command. You could easily give an account the "permission"
`no_tell` to disable their use of this particular command henceforth.
"Permissions" can also be used to block users or implement highly specific bans. The above example would be be added as a lock string to the `tell` command. This will allow everyone *not* having the "permission" `no_tell` to use the `tell` command. You could easily give an account the "permission" `no_tell` to disable their use of this particular command henceforth.
```python
@ -290,9 +230,7 @@ would be be added as a lock string to the `tell` command. This will allow everyo
new_obj.locks.add(lockstring)
```
This is how the `create` command sets up new objects. In sequence, this permission string sets the
owner of this object be the creator (the one running `create`). Builders may examine the object
whereas only Admins and the creator may delete it. Everyone can pick it up.
This is how the `create` command sets up new objects. In sequence, this permission string sets the owner of this object be the creator (the one running `create`). Builders may examine the object whereas only Admins and the creator may delete it. Everyone can pick it up.
### A complete example of setting locks on an object
@ -302,15 +240,11 @@ called `box`.
> create/drop box
> desc box = "This is a very big and heavy box."
We want to limit which objects can pick up this heavy box. Let's say that to do that we require the
would-be lifter to to have an attribute *strength* on themselves, with a value greater than 50. We
assign it to ourselves to begin with.
We want to limit which objects can pick up this heavy box. Let's say that to do that we require the would-be lifter to to have an attribute *strength* on themselves, with a value greater than 50. We assign it to ourselves to begin with.
> set self/strength = 45
Ok, so for testing we made ourselves strong, but not strong enough. Now we need to look at what
happens when someone tries to pick up the the box - they use the `get` command (in the default set).
This is defined in `evennia/commands/default/general.py`. In its code we find this snippet:
Ok, so for testing we made ourselves strong, but not strong enough. Now we need to look at what happens when someone tries to pick up the the box - they use the `get` command (in the default set). This is defined in `evennia/commands/default/general.py`. In its code we find this snippet:
```python
if not obj.access(caller, 'get'):
@ -321,23 +255,17 @@ This is defined in `evennia/commands/default/general.py`. In its code we find th
return
```
So the `get` command looks for a lock with the type *get* (not so surprising). It also looks for an
[Attribute](./Attributes.md) on the checked object called _get_err_msg_ in order to return a customized
error message. Sounds good! Let's start by setting that on the box:
So the `get` command looks for a lock with the type *get* (not so surprising). It also looks for an [Attribute](./Attributes.md) on the checked object called _get_err_msg_ in order to return a customized error message. Sounds good! Let's start by setting that on the box:
> set box/get_err_msg = You are not strong enough to lift this box.
Next we need to craft a Lock of type *get* on our box. We want it to only be passed if the accessing
object has the attribute *strength* of the right value. For this we would need to create a lock
function that checks if attributes have a value greater than a given value. Luckily there is already
such a one included in Evennia (see `evennia/locks/lockfuncs.py`), called `attr_gt`.
Next we need to craft a Lock of type *get* on our box. We want it to only be passed if the accessing object has the attribute *strength* of the right value. For this we would need to create a lock function that checks if attributes have a value greater than a given value. Luckily there is already such a one included in Evennia (see `evennia/locks/lockfuncs.py`), called `attr_gt`.
So the lock string will look like this: `get:attr_gt(strength, 50)`. We put this on the box now:
lock box = get:attr_gt(strength, 50)
Try to `get` the object and you should get the message that we are not strong enough. Increase your
strength above 50 however and you'll pick it up no problem. Done! A very heavy box!
Try to `get` the object and you should get the message that we are not strong enough. Increase your strength above 50 however and you'll pick it up no problem. Done! A very heavy box!
If you wanted to set this up in python code, it would look something like this:

View file

@ -132,7 +132,7 @@ with
> room.add_room_state("summer", "empty")
> room.get_stateful_desc()
This is a nice beach. It is completely empty
This is a nice beach. It is completely empty.
> room.remove_room_state("empty")
> room.add_room_state("full")
@ -140,7 +140,7 @@ with
This is a nice beach. It is full of people.
There are four time-of-day states that are meant to be used with these tags. The
There are four default time-of-day states that are meant to be used with these tags. The
room tracks and changes these automatically. By default they are 'morning',
'afternoon', 'evening' and 'night'. You can get the current time-slot with
`room.get_time_of_day`. You can control them with
@ -148,7 +148,7 @@ room tracks and changes these automatically. By default they are 'morning',
```
ExtendedRoom.hours_per_day # default 24
ExtendedRoom.times_of_day # dict of {season: (start, end), ...} where
# the start/end are given as fractions of the day
# the start/end are given as fractions of the day.
```
You use these inside descriptions as normal:

View file

@ -319,6 +319,8 @@ Most users will want to just treat each map as a location, and name the
"Z-coordinate" things like `Dungeon of Doom`, `The ice queen's palace` or `City
of Blackhaven`. But you could also name it -1, 0, 1, 2, 3 if you wanted.
> Note that the Zcoord is searched *non-case senstively* in the
Pathfinding happens only within each XYMap (up/down is normally 'faked' by moving
sideways to a new area of the XY plane).