Dinemic for Python is wrapper for Dinemic framework, designed for python. As in C++ version, you can create objects in decentralized network, modify them and recreate, at any point of network, without carrying on split brains.

Whole synchronization relays on small messages carrying updates of objects’ state. With digital signatures, data encryption and access control, Dinemic is a great framework for writting small, decentralized applications working without any central points.

Installation

First, install stable version of Dinemic framework. You could get one here, for your favorite distribution:

wget https://packages.dinemic.io/stable/ubuntu-18.04/current/libdinemic_19.08.76639786_amd64.deb
sudo dpkg -i libdinemic_19.08.76639786_amd64.deb

Then install latest version of Dinemic:

sudo apt install python3-pip libboost-python3-dev redis-server ipython3
sudo pip3 install -U dinemic

First script

Install above tools on two separate hosts (laptops, virtual machines, raspberry pi etc). Then on each launch iPython3 environment and execute following commands:

Host A

import dinemic
dinemic.launch()

dinemic.set_loglevel('error')

Host B:

import dinemic
dinemic.launch()

dinemic.set_loglevel('error')

Now, let’s create on both hosts definition of Person model

class Person(dinemic.DModel):
    name = dinemic.DField('name', False)
    salary = dinemic.DField('salary', True)
class Person(dinemic.DModel):
    name = dinemic.DField('name', False)
    salary = dinemic.DField('salary', True)

Name field is not being encrypted (second parameter is False). Salary is confident information, so we want to encrypt that for each person. Model in database will be named the same as class name – Person.

Now let’s create instance of person on first host and try to read its information on second one:

p = Person([])

p.name.set('Bob')
p.salary.set('1234')

print(p.name.get())
# Bob

print(p.salary.get())
# 1234

print(p.get_db_id())
# Person:9uq08fo...

# Let's recreate object from
# it's database ID (see left)
q = Person('Person:9uq08fo...', '')

print(q.name.get())
# Bob
print(q.salary.get())
# Throws exception - no decryption key found

Above example shows how Dinemic shares information across two hosts to use the same object on other machine. As you could see, salary information is available only on host, which owns the object (has its private encryption keys).

Accessing encrypted data of another object

We could change it by creating another object, which will be able to encrypt such confidential data:

chief = Person([])
print(chief.get_db_id())
# Person:a98q034qf...
worker = Person(['Person:a98q034qf...'])
worker.name.set('Bob')
worker.salary.set('44321')

print(worker.get_db_id())
# Person:999fifwre...
my_worker = Person('Person:999fifwre...', 'Person:a98q034qf...)

print(my_worker.salary.get())

Here you can see, that on second host we’ve created the chief object and only recreated my_worker object. Originally encryption keys (thus ownership of this object) is on fist host. In our previous example this made impossible to read by anybody encrypted in that way data (field salary). Here we’ve authorized chief, present on second host to read this data.

Mind, that usage of our constructors was changed here. When creating chef object in first step, we use constructur with empty list, as in first example. But when creating worker object, we add chief’s database ID to this list. This causes, that passed object will be authorized to read encrypted data and update objects (if unauthorized updates are blocked). Finally, on second host we recreate object representing worker, but now we pass to the constructor additional parameter – caller_id. This causes, that Dinemic uses this object to sign all updates and encrypt/decrypt its data.

Using listeners

So, we got basic usage of data models, which share all information with network neighbors, we got access management. Last thing is to protect our model from being modified by unauthorized persons (objects, hosts, etc).

From perspective of data model, each object could be:

  • created
  • field updated
  • field deleted
  • removed

Most of that actions could be distinguished to happen before data is changed (on update) or after data was changed (on updated). Also we can distinguish ownership:

  • object is owned by our host (we have private cryptographic keys present)
  • data change is signed by authorized object (see previous example)
  • data change is not signed or is unauthorized

With that in our mind, we could define various actions (exactly 23) which will be executed, when certain object, its field or whole model is updated.

To create new action, simply define new class:

...

class ListenerPersonCreate(dinemic.DAction):
    def on_create(self, object_id, key):
        print("Hey! New object was created. Its ID is: " + object_id)

Then apply such listener to react only when new object of Person model is created

listener = ListenerPersonCreate()
listener.apply('Person:[id]')

To create listener that is called when authorized update was done on model (and applied on local database copy), create new class:

...

class ListenerPersonUpdated(dinemic.DAction):
    def on_authorized_updated(self, object_id, field, old_value, new_value):
        print("Somebody changed field " + field + " of object " + object_id + " from " + old_value + " to " + new_value)

update_listener = ListenerPersonUpdated()
update_listener.apply('Person:[id]:*')

Here we’ve applied it not to whole model (Person:[id]), but to any subfield of Person model and any object in this model – that is [id] in our filter. You could specify more narrow filters, by adding more sophisticated filters, described here.

And finally, the most important feature of listeners – preventing unauthorized updates of data models. As in second example, we were able to create fields that could be read only by authorized objects (by encrypting it with theirs keys), listeners could help us with rejecting unauthorized updates. Then field could be created as read only, not encrypted:

class ListenerPersonPreventUnauthorized(dinemic.DAction):
    def on_unauthorized_update(self, object_id, field, old_value, new_value):
        print("Ops! Somebody wanted to make unauthorized change")
        raise Exception('Update of field ' + field + ' was blocked')

unauthorized_listener = ListenerPersonPreventUnauthorized()
unauthorized_listener.apply('Person:*')

Now, when such listener is applied, Dinemic will call it each time update is incoming from network. If it has no valid cryptographic signature, then it will be rejected by our listener.

This is very simple example of privileges management with Dinemic. You could create more sophisticated rules to allow unauthorized updates of certain fields or authorize this field yourself, if updates are valid in some specific way.