How-to write plugins ============================ General settings ---------------- The plugin directory must be set in the configuration. Suppose you want your plugins at `/opt/arpoc-plugins`. Then your configuration file must include: .. literalinclude:: ./plugin_config.yml :linenos: :lineno-start: 1 :language: yaml Environment Plugin ------------------ In the AC entities you want to use `environment.foo`. In our simple example this just returns `bar`, but it can be anything. Place the file inside the plugin directory. Note that the filename must end with `.py`. The class of your plugin can be choosen freely, but it must inherit from `arpoc.plugins._lib.EnvironmentAttribute`. The target variable is the attribute key (after `environment.`). On access, the `run()` is executed. .. literalinclude:: ./env_dummy.py :linenos: :lineno-start: 1 :language: python Object Plugin ----------------- We want to show two dependent object setters here. Remember, object setters don't set a single variable but can change the entire objects dictionary. We use the object setters `foo` and `bar`. First, we must enable them in the configuration of the service. So for the service example, you need this configuration. .. code-block:: yaml :caption: Basic configuration file for the objectsetters services: example: [...] objectsetters: foo: enable: true bar: enable: true [...] .. code-block:: python :caption: Python Code for the object setters #filename: /opt/arpoc-plugins/obj_dummy.py from typing import Any, Dict from arpoc.plugins import _lib class ObjFoo(_lib.ObjectSetter): """ Executes the object setter foo """ name = "foo" def __init__(self, cfg: Dict) -> None: super().__init__(cfg) self.cfg = cfg def run(self, data) -> Any: if "foobar" in self.cfg and self.cfg['foobar']: data['foobar'] = True data['foo'] = True return data class ObjBar(_lib.ObjectSetter): """ Executes the object setter bar """ name = "bar" def __init__(self, cfg: Dict) -> None: super().__init__(cfg) self.cfg = cfg def run(self, data) -> Any: if 'foo' in data and data['foo']: data['bar'] = True return data .. code-block:: json :caption: Access Control Entities for the object setters. { "com.example.policysets.default": { "Type": "PolicySet", "Description": "Default Policy Set", "Target": "True", "Policies": ["com.example.policies.default"], "PolicySets": [], "Resolver": "ANY", "Obligations" : [] }, "com.example.policies.default": { "Type": "Policy", "Description": "Default Policy", "Target" : "True", "Rules" : [ "com.example.rules.foo","com.example.rules.foobar","com.example.rules.bar" ], "Resolver": "AND", "Obligations" : [] }, "com.example.rules.foo" : { "Type": "Rule", "Target": "True", "Description": "Default Rule", "Condition" : "object.foo", "Effect": "GRANT", "Obligations" : [] }, "com.example.rules.foobar" : { "Type": "Rule", "Target": "object.path startswith 'foobar'", "Description": "Default Rule", "Condition" : "object.foobar", "Effect": "GRANT", "Obligations" : [] }, "com.example.rules.bar" : { "Type": "Rule", "Target": "object.path startswith 'bar'", "Description": "Default Rule", "Condition" : "object.bar", "Effect": "GRANT", "Obligations" : [] } } With this configuration you can access the paths `foo`, `bar` and `foobar` and the plugins are executed. With `foo` the access will be granted, but `bar` depends that the `foo` is executed beforehand. This is handled by priorities. `foobar` is only set if the configuration is set accordingly. We show this in the next two sections. Priorities ^^^^^^^^^^^ `bar` must be executed after `foo`. To ensure that, you can set the priorities in the configuration file. Plugins with a lower priority will run first. The default priority is 100. We show now a negative example. .. code-block:: yaml :caption: `bar` runs now *before* `foo`, access will be forbidden services: example: [...] objectsetters: foo: enable: true bar: enable: true priority: 99 [...] Of course all ways to set the priority of `foo` lower than `bar` are allowed: #. priority of `foo` < 100, no priority at `bar` #. priority of `bar` > 100, no priority at `foo` #. explicitly set priorities of `foo` < `bar` .. code-block:: yaml :caption: `bar` runs now *after* `foo`, access will be granted services: example: [...] objectsetters: foo: enable: true priority: 98 bar: enable: true priority: 99 [...] Plugin Configuration ^^^^^^^^^^^^^^^^^^^^^ The configuration can be used for any configuration of the object setters. Every key besides `enable` and `priority` can be used. Our plugin uses the key `foobar`. .. code-block:: yaml :caption: Configure `foo` to also set `foobar` services: example: [...] objectsetters: foo: enable: true priority: 98 foobar: true bar: enable: true priority: 99 [...] Obligation Plugin ------------------ The obligations are executed *after* the access control evaluation and *before* the delivery to the user. The obligations are executed when an access control entity lists the obligation and the entity is evaluated and the target specifier evaluated to true. It does not depend on the entity condition evaluation. The obligations are executed without any sorting. The obligation plugin is executed with the access control decision, the evaluation context and its configuration in the configuration file. Example ^^^^^^^^^^^^^^^^^ Suppose you want to print to stderr if the access was granted. We will build the PrintStderr obligation plugin. .. code-block:: json :caption: Access Control Entities with obligations. { "com.example.policysets.default": { "Type": "PolicySet", "Description": "Default Policy Set", "Target": "True", "Policies": ["com.example.policies.default"], "PolicySets": [], "Resolver": "ANY", "Obligations" : ["PrintStderr"] } } .. code-block:: Python :caption: Obligation Plugin. from typing import Any, Dict import sys from arpoc.plugins import _lib from arpoc.ac.common import Effects class OblFoo(_lib.Obligation): """ Executes the object setter foo """ name = "PrintStderr" @staticmethod def run(effect, context: Dict, cfg: Dict) -> bool: if effect == Effects.GRANT: print("Access was granted", file=sys.stderr) return True Configuration & Context ^^^^^^^^^^^^^^^^^^^^^^^ We will alter the example, so that it uses the evaluation context and is configurable. In our example, users with admin as preferred username will not be logged. Denied accesses will be logged if configurred that way. .. code-block:: yaml :caption: Configure `PrintStderr` to also print access tries. services: example: [...] obligations: PrintStderr: printTries: true [...] .. code-block:: Python :caption: Obligation Plugin. @staticmethod def run(effect, context: Dict, cfg: Dict) -> bool: try: if context['subject']['preferred_username'] == 'admin': return True except KeyError: pass if effect == Effects.GRANT: print("Access granted", file=sys.stderr) elif "printTries" in cfg and cfg['printTries']: print("Access was tried, but denied", file=sys.stderr) return True