Refactor access controller#128
Conversation
* added docs and tests for custom verification * fix sync join calls in replication test * added verifyEntry constructor argument * add verifyEntry param to Factory functions. Don't use setter for it
|
@fazo96 would love to hear your feedback too on this. Your PR #123 has been open for a long time (sorry about that! :/) and this one is based on yours but with a very different code as to how to do the custom verification. I believe this would still give you that functionality but with the difference that instead of passing a custom verification function, you'd have to write a custom access controller (which I think would make sense generally if you're planning to check the ACL against a smart contract). |
|
hey @haadcode, thanks for involving me in this :) I think this ACL refactor looks much better designed than my PR and will get me the same functionality. Also thrilled by how easy it looks to pass a custom key to the ACL 👍 One thing I can't wrap my head around is what happens if an ACL is not passed? No more signing of the entries by default? I'm probably just missing something |
I left out that part and we should discuss what the default behaviour should be. You're correct that right now the entries don't get signed if an acl is not passed (which prolly doesn't have a lot of use cases as it breaks the integrity of the log). I suppose we could either throw an error or defer to a default access controller (like it used to be / is now). Any thoughts? @fazo96 do you have any links to how you're using the custom verification in your code? Would love to understand your use case better so that we can take that into consideration designing all this. |
|
@haadcode sure, here is the custom OrbitDB Store we use in the current Chlu implementation: https://github.com/ChluNetwork/chlu-ipfs-support/blob/master/src/modules/orbitdb/store.js For our validation needs, there's three points at which we can validate content before accepting it:
We haven't much validation going on related to orbit-db data, but the ReviewRecords we store are supposed to be independently validatable objects and they are, so the validation for now occures in the code that comes after reading from the orbit-db index (point 4). This is not ideal because the index can get polluted by any fake information, and also we allow review records to be updated and the current implementation makes it possible for anyone to create a bogus update and thus making the review record invalid for everyone. The point of my PR #127 was to make point 1 possible and orbitdb-archive/orbit-db-store#21 already made point 2 possible. point 3 is easy because we have a custom index and point 4 does not involve orbit-db. So we have an external, async validation function that can be called with the CID of the review record (passed in the payload of the orbit-db operation). This function will return true if the data is valid and should be kept in orbit-db. In conclusion, our use case was making sure we have the means of keeping invalid data out of the index or even out of the oplog, depends on the user/application of our Chlu protocol (maybe it is preferable to keep invalid data in the oplog for further analysis and debugging, for example, but still keep it out of the index). Another important point in favour is that to authenticate users we use a Decentralized Identifier (DID) that has its own keypair and the custom ACL in this PR makes it easier for us to reuse DID keypairs for orbit-db signing and validating. |
| * @return {Promise<Log>} New Log | ||
| */ | ||
| static fromEntry (ipfs, sourceEntries, length = -1, exclude, onProgressCallback) { | ||
| static fromEntry (ipfs, sourceEntries, length = -1, exclude, onProgressCallback, verifyEntry) { |
There was a problem hiding this comment.
Log no longer takes verifyEntry, and so neither should fromEntry. That argument can be removed.
There was a problem hiding this comment.
Thank you! 👍Will fix all the verifyEntry args as per your comments.
| // TODO: need to verify the entries with 'key' | ||
| return LogIO.fromEntry(ipfs, sourceEntries, length, exclude, onProgressCallback) | ||
| .then((data) => new Log(ipfs, data.id, data.values)) | ||
| .then((data) => new Log(ipfs, data.id, data.values, null, null, null, verifyEntry)) |
There was a problem hiding this comment.
And same here, Log now takes six arguments, with the seventh being an ACL.
| * @return {Promise<Log>} New Log | ||
| */ | ||
| static fromMultihash (ipfs, hash, length = -1, exclude, key, onProgressCallback) { | ||
| static fromMultihash (ipfs, hash, length = -1, exclude, acl, onProgressCallback, verifyEntry) { |
| * @param {string} hash Multihash (as a Base58 encoded string) to create the log from | ||
| * @param {Number} [length=-1] How many items to include in the log | ||
| * @param {Function(hash, entry, parent, depth)} onProgressCallback | ||
| * @param {Function} [verifyEntry] Custom entry verification function |
| // TODO: need to verify the entries with 'key' | ||
| return LogIO.fromMultihash(ipfs, hash, length, exclude, onProgressCallback) | ||
| .then((data) => new Log(ipfs, data.id, data.values, data.heads, data.clock, key)) | ||
| .then((data) => new Log(ipfs, data.id, data.values, data.heads, data.clock, acl, verifyEntry)) |
| * @return {Promise<Log>} New Log | ||
| */ | ||
| static fromEntryHash (ipfs, hash, id, length = -1, exclude, key, keys, onProgressCallback) { | ||
| static fromEntryHash (ipfs, hash, id, length = -1, exclude, acl, onProgressCallback, verifyEntry) { |
| * @param {string} hash Multihash (as a Base58 encoded string) of the Entry from which to create the log from | ||
| * @param {Number} [length=-1] How many entries to include in the log | ||
| * @param {Function(hash, entry, parent, depth)} onProgressCallback | ||
| * @param {Function} [verifyEntry] Custom entry verification function |
| * @return {Promise<Log>} New Log | ||
| */ | ||
| static fromJSON (ipfs, json, length = -1, key, keys, timeout, onProgressCallback) { | ||
| static fromJSON (ipfs, json, length = -1, acl, timeout, onProgressCallback, verifyEntry) { |
| return LogIO.fromJSON(ipfs, json, length, key, timeout, onProgressCallback) | ||
| .then((data) => new Log(ipfs, data.id, data.values, null, null, key, keys)) | ||
| return LogIO.fromJSON(ipfs, json, length, timeout, onProgressCallback) | ||
| .then((data) => new Log(ipfs, data.id, data.values, null, null, acl, verifyEntry)) |
| @@ -198,20 +200,13 @@ class Log extends GSet { | |||
| * @return {Log} New Log containing the appended value | |||
| */ | |||
| async append (data, pointerCount = 1) { | |||
There was a problem hiding this comment.
[edit: this comment previously worried that we weren't checking for write permissions in append, but now I see that Entry.create calls sign which calls canAppend 👍 ]
There was a problem hiding this comment.
Your previous comment was a good point though. I was thinking if we should make it (calling canAppend or sign) explicit and call it in the log instead of in Entry?
There was a problem hiding this comment.
I already forgot it a second time 😂 so we could either make it explicit, or make a generalized testing suite for the access controller API to ensure that all access controllers conform to this API.
|
Do you intend The smart contract access controller needs to do both, because it can't provide a list of keys. I suppose we'll just pass down the smart contract access controller. |
|
|
||
| static async signEntry (keystore, entry, key) { | ||
| const signature = await keystore.sign(key, Buffer.from(JSON.stringify(entry))) | ||
| static async sign (entry, acl) { |
There was a problem hiding this comment.
A smart contract access controller will want to augment the entry with a chainSignature and chainPublicKey, or something like that.
So perhaps acl.sign should return the entry, and the default acl is responsible for adding the key and signature.
| @@ -0,0 +1,53 @@ | |||
| class ACL { | |||
| constructor (keystore, key, keys = []) { | |||
| this._keystore = keystore | |||
There was a problem hiding this comment.
I think here it should only get sign, getPubKey and verify functions instead of direct access to the keys. We should keep sign, getPubKey and verify here though so we can handle encoding/parsing on ipfs-log
| @@ -0,0 +1,53 @@ | |||
| class ACL { | |||
There was a problem hiding this comment.
Maybe we could call it IntegrityChecker to avoid confusion around orbit-db vs ipfs-log ACL
| return this._key.getPublic(format) | ||
| } | ||
|
|
||
| canAppend (signingKey) { |
There was a problem hiding this comment.
If we pass functions from orbit-db access controller to it, we wouldn't need this anymore
| const nexts = Object.keys(this.traverse(this.heads, pointerCount)) | ||
| // Create the entry and add it to the internal cache | ||
| const entry = await Entry.create(this._storage, this._keystore, this.id, data, nexts, this.clock, this._key) | ||
| const entry = await Entry.create(this._storage, this._acl, this.id, data, nexts, this.clock) |
There was a problem hiding this comment.
Instead of passing the whole ACL object here, the Entry.create method could be broken up into create/sign/put. It might make it easier to understand how it actually works – since one might assume this is simply creating an entry instance – and then we can pass just the public key to be used as a fallback for the clockId property. Maybe a TODO for another PR?
| return this._key | ||
| } | ||
|
|
||
| getPublicSigningKey (format = 'hex') { |
There was a problem hiding this comment.
I think ipfs-log always uses this format. Is it going to change or can we assume it'll always be 'hex'?
| return entries.reduce(reduceTailHashes, []) | ||
| } | ||
|
|
||
| static difference (a, b) { |
There was a problem hiding this comment.
Is it similar to utils/difference?
| this._key = key | ||
| this._keys = Array.isArray(keys) ? keys : [keys] | ||
| // Access Controller | ||
| this._acl = acl |
There was a problem hiding this comment.
Like @fazo96 mentioned in his comment (#128 (comment)), maybe we should add some validation here that checks if the ACL exists and if it follows the "interface" we need. If it doesn't, we throw an error. Is there a scenario where it wouldn't be necessary?
| if (keystore && signKey) { | ||
| entry = await Entry.signEntry(keystore, entry, signKey) | ||
| // If acl was passed, sign the entry (=authorize) | ||
| if (acl) { |
There was a problem hiding this comment.
I think this should be a breaking change, providing an ACL should be mandatory from now on
|
Closing as obsolete in favor of #159. |
This PR refactors the access controller in a way that the ACL handles all keys, signing and verification related functionality. The general idea is that Log/Entry don't know anything about the keys or keystore and only gets and
aclobject (the access controller) which encapsulates the functionality.I did the previous commits in a bit of a rush, so there may be some weirdness here. Would love to hear your feedback @coyotespike, @shamb0t and @vvp.
I'm unsure if the ACL api is what we want to do in the end, but at least this would give us a "clean" separation of the acl and the log.
I'll try to check this again myself in the next couple of weeks to make sure everything makes sense, but for now, I hope this helps you to proceed with the refactoring / acl work that you're doing.