JoinColony / colonyDapp

Colony dApp client
49 stars 19 forks source link

DloD: DDB's list of Death #108

Closed coyotespike closed 5 years ago

coyotespike commented 6 years ago

This is the minimum set of issues we gotta close to use orbit the way we intend to

Please see this gist for a more complete breakdown. In particular, section 3 explains the architecture

collinvine commented 5 years ago

Copying the gist below for posterity.


User Stories

  1. As a user, I want to store my information and access colony information.
  2. As a user, I want to be able to change machines, and still access my profile and change boards.
  3. As a user, I want to add and remove people from boards.

Progress on Each

The dApp-Orbit integration work involves data modeling. The implementation is similar, at least in principle, to the PoC we've already completed. This is known territory.

The pinningService is mostly complete. It is ready to be improved through usage.

The solution to keys is WIP. We have ideas but they are not yet proven.

Dynamic database permissions has several suggested implementations, but is not yet implemented, and there's a fair amount of work to do. This is a known unknown.

1. Keys

Keystore

IPFS-log interface to contract and DDB

Generating keys

Wallets

From Thiago's Notes:

Wallets we need to cater for specifically

Generating keys, notes from Thiago's research

2. Permissions

OrbitDB issue

DSGuard Contract

shamb0t's example

Access Controller

Laurent's plan

Permissions Model

3. Architecture

This architecture, once fully understood, largely explains the proposed changes :-)

OrbitDB.js uses ipfs-access-controller (which inherits from access-controller) to create and save into IPFS an access-controller.

  1. OrbitDB.js uses keystore.js to create a key, and puts this in the ipfs-access-controller array.
  2. The ipfs-access-controller is saved to IPFS, and its hash is then included in the new database hash. For that reason the permissions are immutable. Change permissions, and the database hash changes. This is an elegantly simple security model.
  3. the access controller (and key) are passed through the database types to orbit-db-store.
  4. orbit-db-store double-checks OrbitDB's work, using the keystore and key if passed down, and if not also creating defaults.
  5. orbit-db-store passes this key, and the access array, to ipfs-log.
  6. ipfs-log checks if the key is in the access array, and if so adds an entry.
  7. ipfs-log/entry.js uses the keystore and key to sign and verify entries.

Further Notes

ipfs-log/log.js, log.append, checks for write permissions. It doesn't verify, but decorates the entry.

ipfs-log/log.js, log.join, checks for write permissions, and also verifies each entry.

4. Plan of Attack

Permissions

Keystore and Keys

5. Future Database Changes

Orbit

collinvine commented 5 years ago

Laurent's Gist on OrbitDB Permissions for posterity.


The idea of dogfooding for permissions would be perfect to deal with external systems.

One limitation I see is that it limits us to using orbitdb-backed approach, which might not be as fine grained as we'd like to.

For example, we would like to:

A simple approach would be to let app developpers customize the create entry and the validate entry operations:

Could orbitdb implement an inversion of control / middleware approach to let us define different permission protocols?

the IPFS manifest and the (wip) store approaches would be provided as default middlewares. We would be able to define access control specific to our needs & combine them.

Auth-related code would be refactored out of orbitdb ipfs-logs to separate concerns more clearly:

- IPFS: store & Exchange Data
- Orbit Log: update & merge operations
- Access Middleware: add auth info to updates & validate their permissions.

The access middleware would look something like:

interface AccessMiddleware:
  isWriteAllowed: (entry) => bool
  - Accept or reject entries

  decorateEntry: (entry) => entry
  - Update an entry payload with permission informations

A few practical examples:

Scenario: Allow everything.

class AccessControllerAllowAll {
  this.isWriteAllowed = async (entry) => true
  this.decorateEntry = async (entry) => entry
}

Scenario: Allow based on ipfs manifest:

class AccessControllerIPFSManifest {
  constructor(key, ipfs, manifestAddress) {
    this.keyStore = keyStore;
    this.manifest = await loadManifest(ipfs, manifestAddress)
  }

  async isWriteAllowed(entry) {
    if (!this.keyStore.verify(entry, entry.signature) === entry.key) {
      return false; // wrong signature
    }

    // manifest allows this key
    return (this.manifest.writes.include('*') || this.manifest.writes.include(entry.key));
  }

  async decorateEntry(entry) {
    entry.key = this.keyStore.publicKey;
    entry.signature = this.keyStore.sign(entry);
  }
}

Scenario: Allow writes depending on roles defined on chain.

class AccessControllerAllowFromBlockchain {
  async isWriteAllowed(entry) {
    if (!(await myBlockchainAPI.isUserAdmin(entry.key))) {
      return false;
    }

    return this.keyStore.verify(entry, entry.signature) === entry.key;
  }

  decorateEntry: (entry) => {
    entry.key = this.keyStore.publicKey;
    entry.signature = this.keyStore.sign(entry);
    return entry;
}

Scenario: We want roles defined on-chain: "I'm the owner of a Colony", "I'm the owner of a user ID". AND we want users to be able to change devices. We don't want to have them sign every orbitdb update with their hardware / metamask wallet.

For this we'd need to verify that a payload was signed by a key (localKey) and that this localKey is owned by the owner of an ethereum address (globalKey).

const AccessControllerWithChildKeys {
  constructor(rootKeyStore) {
    this.localKeyStore = generateKeyPairs();
    this.localKeySignature = rootKeyStore.sign(this.childKeyStore.publicKey); // happens once
  }

  async isWriteAllowed(entry) {
    // Check the payload was signed by a given local key
    const verified = this.localKeyStore.verify(entry, entry.signature) === entry.key;

    if (!verified) {
      return false;
    }

    // Check that the local key was signed by the owner of a ressource on chain
    const globalKey = this.keyStore.verify(entry.key, entry.rootSignature);
    return (await myBlockchainAPI.isUserAdmin(globalKey))
  }

  async decorateEntry(entry) {
    entry.key = this.keyStore.publicKey;
    entry.signature = this.localKeyStore.sign(entry);
    entry.rootSignature = this.localKeySignature;
    return entry;
  }
}

We'd pass a controller, either as an instance, or as a factory (that takes this.ipfs and other parameters).

function myStore() {
  const accessController = new AccessControllerWithChildKeys(rootKeyStore);
  const store = orbitdb.kvstore({accessController});
}

This would let us:

Limitations:

Code change (WIP):

collinvine commented 5 years ago

Thiago notes.md


Wallets we need to cater for specifically

NOTE: We can’t plug in a wallet into orbit. Taking actions to sign every entry, it’d be a terrible experience for users and we need to be able to revoke keys, doing that per ethereum account would get pretty annoying too.

Permissions

NOTE: In the case that ethereum node isn’t up-to-date, it might be necessary to put new entries in some kind of queue to be retried later otherwise they’d get invalidated

NOTE2: Is it possible for us to have a contract that just generates a unique id per entry and sign it if the wallet plugged to the contract is allowed to do the change?

NOTE3: orbit-db-keystore, ipfs-log and access controllers should be changed. ipfs-log shouldn’t know about keystore implementation details (use sign and verify functions passed via constructor to the log instead), the keystore has a few unused methods, the access controller should get the keystore and interface with the ipfs-log instead of passing the keystore to the log

Keys

NOTE: Currently, orbit stores users keys on the localStorage. That needs to change!

In the ideal scenario, we generate a master-key from an ethereum account that will be used to manage permissions on the blockchain and that master-key generates child keys that are used to sign entries on orbit (per db instance or per dapp)