From 428ce838781a326987b041476da9a5c2c68bd438 Mon Sep 17 00:00:00 2001 From: Hayden Young Date: Mon, 3 Apr 2023 19:56:47 +0800 Subject: [PATCH] Refactor/ac (#61) * refactor: Move Manifest to own module. * refactor: Modularize orbitdb access controller. * chore: Check for correct access controller path and modify if necessary. * fix: Linting. * refactor: AC interface no longer needed. * refactor: Move IPFS-specific AC list back into IPFS AC. * refactor: Explicitly name access controller param. * refactor: Pass in manifest settings as object. * refactor: Config access controllers. * refactor: ACs should expose specific params before being called with generic params. * feat: Pass write access to root IPFS AC. * refactor: AC should handle type prefix. * test: Test for type. * refactor: Pass generic access to Database (and inheriting dbs). * refactor: Use AccessControllers module to manage custom ACs. * chore: Remove excess console logging. * test: Fix ipfs module import. --- src/OrbitDB.js | 38 +- src/access-controllers/index.js | 94 ++-- src/access-controllers/interface.js | 50 -- src/access-controllers/ipfs.js | 22 +- src/access-controllers/manifest.js | 33 -- src/access-controllers/orbitdb.js | 178 +++--- src/database.js | 7 +- src/db/document-store.js | 4 +- src/db/event-store.js | 4 +- src/db/keyvalue-persisted.js | 4 +- src/db/keyvalue.js | 4 +- src/manifest.js | 7 +- src/utils/ensure-ac-address.js | 9 + .../access-controller-handlers.test.js | 259 +++++---- .../ipfs-access-controller.test.js | 115 ++-- .../orbit-db-access-controller.test.js | 531 ++++++++---------- test/manifest.test.js | 33 +- test/orbitdb-access-controllers.test.js | 115 ++++ test/orbitdb-custom-database-types.test.js | 2 +- test/orbitdb-write-access.test.js | 61 +- 20 files changed, 781 insertions(+), 789 deletions(-) delete mode 100644 src/access-controllers/interface.js delete mode 100644 src/access-controllers/manifest.js create mode 100644 src/utils/ensure-ac-address.js create mode 100644 test/orbitdb-access-controllers.test.js diff --git a/src/OrbitDB.js b/src/OrbitDB.js index cbdd2fb..ceabf5e 100644 --- a/src/OrbitDB.js +++ b/src/OrbitDB.js @@ -4,7 +4,6 @@ import { Log, Entry } from './oplog/index.js' import { ComposedStorage, IPFSBlockStorage, LevelStorage, LRUStorage } from './storage/index.js' import KeyStore from './key-store.js' import { Identities } from './identities/index.js' -import IPFSAccessController from './access-controllers/ipfs.js' import OrbitDBAddress, { isValidAddress } from './address.js' import DBManifest from './manifest.js' import { createId } from './utils/index.js' @@ -13,6 +12,7 @@ import pathJoin from './utils/path-join.js' import * as Block from 'multiformats/block' import * as dagCbor from '@ipld/dag-cbor' import { sha256 } from 'multiformats/hashes/sha2' +import * as AccessControllers from './access-controllers/index.js' const codec = dagCbor const hasher = sha256 @@ -26,6 +26,11 @@ const databaseTypes = { documents: DocumentStore, keyvalue: KeyValue } +// +// const accessControllers = { +// ipfs: IPFSAccessController, +// orbitdb: OrbitDBAccessController +// } const addDatabaseType = (type, store) => { if (databaseTypes[type]) { @@ -33,6 +38,13 @@ const addDatabaseType = (type, store) => { } databaseTypes[type] = store } +// +// const addAccessController = (type, accessController) => { +// if (accessControllers[type]) { +// throw new Error(`Access Controller already exists: ${type}`) +// } +// accessControllers[type] = accessController +// } // const defaultTimeout = 30000 // 30 seconds @@ -57,11 +69,11 @@ const OrbitDB = async ({ ipfs, id, identity, keystore, directory } = {}) => { let databases = {} - const open = async (address, { type, meta, write, sync, Store } = {}) => { + const open = async (address, { type, meta, sync, Store, AccessController } = {}) => { let name, manifest, accessController if (type && !databaseTypes[type]) { - throw new Error(`Unspported database type: '${type}'`) + throw new Error(`Unsupported database type: '${type}'`) } if (databases[address]) { @@ -74,19 +86,23 @@ const OrbitDB = async ({ ipfs, id, identity, keystore, directory } = {}) => { const bytes = await manifestStorage.get(addr.path) const { value } = await Block.decode({ bytes, codec, hasher }) manifest = value - const acAddress = manifest.accessController.replaceAll('/ipfs/', '') - accessController = await IPFSAccessController({ ipfs, identities, identity, address: acAddress, storage: manifestStorage, write }) + const acType = manifest.accessController.split('/', 2).pop() + + const acAddress = manifest.accessController.replaceAll(`/${acType}/`, '') + accessController = await AccessControllers.get(acType)()({ orbitdb: { open, identity, ipfs }, identities, address: acAddress }) name = manifest.name type = type || manifest.type meta = manifest.meta } else { // If the address given was not valid, eg. just the name of the database type = type || 'events' - accessController = await IPFSAccessController({ ipfs, identities, identity, storage: manifestStorage, write }) - const m = await DBManifest(manifestStorage, address, type, accessController.address, { meta }) + AccessController = AccessController || AccessControllers.get('ipfs')({ storage: manifestStorage }) + accessController = await AccessController({ orbitdb: { open, identity, ipfs }, identities }) + const m = await DBManifest({ storage: manifestStorage, name: address, type, accessController: accessController.address, meta }) + manifest = m.manifest address = OrbitDBAddress(m.hash) - accessController = m.accessController + // accessController = manifest.accessController name = manifest.name meta = manifest.meta } @@ -94,10 +110,10 @@ const OrbitDB = async ({ ipfs, id, identity, keystore, directory } = {}) => { const DatabaseModel = Store || databaseTypes[type] if (!DatabaseModel) { - throw new Error(`Unspported database type: '${type}'`) + throw new Error(`Unsupported database type: '${type}'`) } - const db = await DatabaseModel({ OpLog, Database, ipfs, identity, address: address.toString(), name, accessController, directory, meta, syncAutomatically: sync != null ? sync : true }) + const db = await DatabaseModel({ OpLog, Database, ipfs, identity, address: address.toString(), name, access: accessController, directory, meta, syncAutomatically: sync != null ? sync : true }) db.events.on('close', onDatabaseClosed(address.toString())) @@ -131,7 +147,7 @@ const OrbitDB = async ({ ipfs, id, identity, keystore, directory } = {}) => { } } -export { OrbitDB as default, OrbitDBAddress, addDatabaseType, databaseTypes } +export { OrbitDB as default, OrbitDBAddress, addDatabaseType, databaseTypes, AccessControllers } // class OrbitDB2 { // constructor (ipfs, identity, options = {}) { diff --git a/src/access-controllers/index.js b/src/access-controllers/index.js index 9b62ecc..2313a40 100644 --- a/src/access-controllers/index.js +++ b/src/access-controllers/index.js @@ -1,70 +1,42 @@ -import AccessController from './interface.js' -import AccessControllerManifest from './manifest.js' -// import LegacyIPFSAccessController from './access-controllers/legacy-ipfs.js' import IPFSAccessController from './ipfs.js' -// import OrbitDBAccessController from './orbitdb.js' +import OrbitDBAccessController from './orbitdb.js' -const supportedTypes = { - // 'legacy-ipfs': LegacyIPFSAccessController, - ipfs: IPFSAccessController - // orbitdb: OrbitDBAccessController +const types = { + ipfs: IPFSAccessController, + orbitdb: OrbitDBAccessController } -const getHandlerFor = (type) => { - if (!AccessControllers.isSupported(type)) { +const get = (type) => { + if (!isSupported(type)) { throw new Error(`AccessController type '${type}' is not supported`) } - return supportedTypes[type] + return types[type] } -export default class AccessControllers { - static get AccessController () { return AccessController } - - static isSupported (type) { - return Object.keys(supportedTypes).includes(type) - } - - static addAccessController (options) { - if (!options.AccessController) { - throw new Error('AccessController class needs to be given as an option') - } - - if (!options.AccessController.type || - typeof options.AccessController.type !== 'string') { - throw new Error('Given AccessController class needs to implement: static get type() { /* return a string */}.') - } - - supportedTypes[options.AccessController.type] = options.AccessController - } - - static addAccessControllers (options) { - const accessControllers = options.AccessControllers - if (!accessControllers) { - throw new Error('AccessController classes need to be given as an option') - } - - accessControllers.forEach((accessController) => { - AccessControllers.addAccessController({ AccessController: accessController }) - }) - } - - static removeAccessController (type) { - delete supportedTypes[type] - } - - static async resolve (orbitdb, manifestAddress, options = {}) { - const { type, params } = await AccessControllerManifest.resolve(orbitdb._ipfs, manifestAddress, options) - const AccessController = getHandlerFor(type) - const accessController = await AccessController.create(orbitdb, Object.assign({}, options, params)) - await accessController.load(params.address) - return accessController - } - - static async create ({ ipfs, identity }, type, options = {}) { - const AccessController = getHandlerFor(type) - const ac = await AccessController.create({ ipfs, identity }, options) - const params = await ac.save() - const hash = await AccessControllerManifest.create(ipfs, type, params) - return hash - } +const isSupported = type => { + return Object.keys(types).includes(type) +} + +const add = (accessController) => { + if (types[accessController.type]) { + throw new Error(`Access controller '${accessController.type}' already added.`) + } + + if (!accessController.type) { + throw new Error('Given AccessController class needs to implement: type.') + } + + types[accessController.type] = accessController +} + +const remove = type => { + delete types[type] +} + +export { + types, + get, + isSupported, + add, + remove } diff --git a/src/access-controllers/interface.js b/src/access-controllers/interface.js deleted file mode 100644 index d5c7a30..0000000 --- a/src/access-controllers/interface.js +++ /dev/null @@ -1,50 +0,0 @@ -import { EventEmitter } from 'events' - -/** - * Interface for OrbitDB Access Controllers - * - * Any OrbitDB access controller needs to define and implement - * the methods defined by the interface here. - */ -export default class AccessController extends EventEmitter { - /* - Every AC needs to have a 'Factory' method - that creates an instance of the AccessController - */ - static async create (orbitdb, options) {} - - /* Return the type for this controller */ - static get type () { - throw new Error('\'static get type ()\' needs to be defined in the inheriting class') - } - - /* - Return the type for this controller - NOTE! This is the only property of the interface that - shouldn't be overridden in the inherited Access Controller - */ - get type () { - return this.constructor.type - } - - /* Each Access Controller has some address to anchor to */ - get address () {} - - /* - Called by the databases (the log) to see if entry should - be allowed in the database. Return true if the entry is allowed, - false is not allowed - */ - async canAppend (entry, identityProvider) {} - - /* Add and remove access */ - async grant (access, identity) { return false } - async revoke (access, identity) { return false } - - /* AC creation and loading */ - async load (address) {} - /* Returns AC manifest parameters object */ - async save () {} - /* Called when the database for this AC gets closed */ - async close () {} -} diff --git a/src/access-controllers/ipfs.js b/src/access-controllers/ipfs.js index 36ab84a..2c66431 100644 --- a/src/access-controllers/ipfs.js +++ b/src/access-controllers/ipfs.js @@ -1,19 +1,15 @@ -// import * as io from '../utils/index.js' -// import AccessController from './interface.js' -// import AccessControllerManifest from './manifest.js' import { IPFSBlockStorage } from '../storage/index.js' import * as Block from 'multiformats/block' import * as dagCbor from '@ipld/dag-cbor' import { sha256 } from 'multiformats/hashes/sha2' import { base58btc } from 'multiformats/bases/base58' +import pathJoin from '../utils/path-join.js' const codec = dagCbor const hasher = sha256 const hashStringEncoding = base58btc -const type = 'ipfs' - -const AccessControllerManifest = async ({ storage, type, params }) => { +const AccessControlList = async ({ storage, type, params }) => { const manifest = { type, ...params @@ -24,17 +20,19 @@ const AccessControllerManifest = async ({ storage, type, params }) => { return hash } -const IPFSAccessController = async ({ ipfs, identities, identity, address, storage, write }) => { - storage = storage || await IPFSBlockStorage({ ipfs, pin: true }) +const type = 'ipfs' - write = write || [identity.id] +const IPFSAccessController = ({ write, storage } = {}) => async ({ orbitdb, identities, address }) => { + storage = storage || await IPFSBlockStorage({ ipfs: orbitdb.ipfs, pin: true }) + write = write || [orbitdb.identity.id] if (address) { - const manifestBytes = await storage.get(address) + const manifestBytes = await storage.get(address.replaceAll('/ipfs/', '')) const { value } = await Block.decode({ bytes: manifestBytes, codec, hasher }) write = value.write } else { - address = await AccessControllerManifest({ storage, type, params: { write } }) + address = await AccessControlList({ storage, type, params: { write } }) + address = pathJoin('/', type, address) } const canAppend = async (entry) => { @@ -59,4 +57,4 @@ const IPFSAccessController = async ({ ipfs, identities, identity, address, stora } } -export { IPFSAccessController as default } +export default IPFSAccessController diff --git a/src/access-controllers/manifest.js b/src/access-controllers/manifest.js deleted file mode 100644 index bf480ce..0000000 --- a/src/access-controllers/manifest.js +++ /dev/null @@ -1,33 +0,0 @@ -// import * as io from 'orbit-db-io' - -// export default class AccessControllerManifest { -// constructor (type, params = {}) { -// this.type = type -// this.params = params -// } - -// static async resolve (ipfs, manifestHash, options = {}) { -// if (options.skipManifest) { -// if (!options.type) { -// throw new Error('No manifest, access-controller type required') -// } -// return new AccessControllerManifest(options.type, { address: manifestHash }) -// } else { -// // TODO: ensure this is a valid multihash -// if (manifestHash.indexOf('/ipfs') === 0) { manifestHash = manifestHash.split('/')[2] } -// const { type, params } = await io.read(ipfs, manifestHash) -// return new AccessControllerManifest(type, params) -// } -// } - -// static async create (ipfs, type, params) { -// if (params.skipManifest) { -// return params.address -// } -// const manifest = { -// type, -// params -// } -// return io.write(ipfs, 'dag-cbor', manifest) -// } -// } diff --git a/src/access-controllers/orbitdb.js b/src/access-controllers/orbitdb.js index c65393a..30d55a6 100644 --- a/src/access-controllers/orbitdb.js +++ b/src/access-controllers/orbitdb.js @@ -1,134 +1,108 @@ -import pMapSeries from 'p-map-series' -import AccessController from './interface.js' -import ensureAddress from '../utils/ensure-ac-address.js' +import { EventEmitter } from 'events' +import ensureACAddress from '../utils/ensure-ac-address.js' +import IPFSAccessController from './ipfs.js' const type = 'orbitdb' -export default class OrbitDBAccessController extends AccessController { - constructor (orbitdb, options) { - super() - this._orbitdb = orbitdb - this._db = null - this._options = options || {} +const OrbitDBAccessController = ({ write } = {}) => async ({ orbitdb, identities, address }) => { + const events = new EventEmitter() + + address = address || 'default-access-controller' + write = write || [orbitdb.identity.id] + + // Force '
/_access' naming for the database + const db = await orbitdb.open(ensureACAddress(address), { type: 'keyvalue', AccessController: IPFSAccessController({ write }) }) + address = db.address + + const onUpdate = (entry) => { + events.emit('update', entry) } - // Returns the type of the access controller - static get type () { return type } - - // Returns the address of the OrbitDB used as the AC - get address () { - return this._db.address - } + db.events.on('update', onUpdate) // Return true if entry is allowed to be added to the database - async canAppend (entry, identityProvider) { - // Write keys and admins keys are allowed - const access = new Set([...this.get('write'), ...this.get('admin')]) + const canAppend = async (entry) => { + const writerIdentity = await identities.getIdentity(entry.identity) + if (!writerIdentity) { + return false + } + + const { id } = writerIdentity // If the ACL contains the writer's public key or it contains '*' - if (access.has(entry.identity.id) || access.has('*')) { - const verifiedIdentity = await identityProvider.verifyIdentity(entry.identity) - // Allow access if identity verifies - return verifiedIdentity + const hasWriteAccess = await hasCapability('write', id) || await hasCapability('admin', id) + if (hasWriteAccess) { + return identities.verifyIdentity(writerIdentity) } return false } - get capabilities () { - if (this._db) { - const capabilities = this._db.index - - const toSet = (e) => { - const key = e[0] - capabilities[key] = new Set([...(capabilities[key] || []), ...e[1]]) - } - - // Merge with the access controller of the database - // and make sure all values are Sets - Object.entries({ - ...capabilities, - // Add the root access controller's 'write' access list - // as admins on this controller - ...{ admin: new Set([...(capabilities.admin || []), ...this._db.access.write]) } - }).forEach(toSet) - - return capabilities + const capabilities = async () => { + const _capabilities = [] + for await (const entry of db.iterator()) { + _capabilities[entry.key] = entry.value } - return {} - } - get (capability) { - return this.capabilities[capability] || new Set([]) - } - - async close () { - await this._db.close() - } - - async load (address) { - if (this._db) { await this._db.close() } - - // Force '
/_access' naming for the database - this._db = await this._orbitdb.keyvalue(ensureAddress(address), { - // use ipfs controller as a immutable "root controller" - accessController: { - type: 'ipfs', - write: this._options.admin || [this._orbitdb.identity.id] - }, - sync: true - }) - - this._db.events.on('ready', this._onUpdate.bind(this)) - this._db.events.on('write', this._onUpdate.bind(this)) - this._db.events.on('replicated', this._onUpdate.bind(this)) - - await this._db.load() - } - - async save () { - // return the manifest data - return { - address: this._db.address.toString() + const toSet = (e) => { + const key = e[0] + _capabilities[key] = new Set([...(_capabilities[key] || []), ...e[1]]) } + + // Merge with the access controller of the database + // and make sure all values are Sets + Object.entries({ + ..._capabilities, + // Add the root access controller's 'write' access list + // as admins on this controller + ...{ admin: new Set([...(_capabilities.admin || []), ...write]) } + }).forEach(toSet) + + return _capabilities } - async hasCapability (capability, identity) { + const get = async (capability) => { + const _capabilities = await capabilities() + return _capabilities[capability] || new Set([]) + } + + const close = async () => { + await db.close() + } + + const hasCapability = async (capability, key) => { // Write keys and admins keys are allowed - const access = new Set(this.get(capability)) - return access.has(identity.id) || access.has('*') + const access = new Set(await get(capability)) + return access.has(key) || access.has('*') } - async grant (capability, key) { + const grant = async (capability, key) => { // Merge current keys with the new key - const capabilities = new Set([...(this._db.get(capability) || []), ...[key]]) - await this._db.put(capability, Array.from(capabilities.values())) + const capabilities = new Set([...(await db.get(capability) || []), ...[key]]) + await db.put(capability, Array.from(capabilities.values())) } - async revoke (capability, key) { - const capabilities = new Set(this._db.get(capability) || []) + const revoke = async (capability, key) => { + const capabilities = new Set(await db.get(capability) || []) capabilities.delete(key) if (capabilities.size > 0) { - await this._db.put(capability, Array.from(capabilities.values())) + await db.put(capability, Array.from(capabilities.values())) } else { - await this._db.del(capability) + await db.del(capability) } } - /* Private methods */ - _onUpdate () { - this.emit('updated') - } - - /* Factory */ - static async create (orbitdb, options = {}) { - const ac = new OrbitDBAccessController(orbitdb, options) - await ac.load(options.address || options.name || 'default-access-controller') - - // Add write access from options - if (options.write && !options.address) { - await pMapSeries(options.write, async (e) => ac.grant('write', e)) - } - - return ac + return { + type, + address, + write, + canAppend, + capabilities, + get, + grant, + revoke, + close, + events } } + +export default OrbitDBAccessController diff --git a/src/database.js b/src/database.js index 546c526..1fcd9a9 100644 --- a/src/database.js +++ b/src/database.js @@ -7,7 +7,7 @@ import pathJoin from './utils/path-join.js' const defaultReferencesCount = 16 const defaultCacheSize = 1000 -const Database = async ({ OpLog, ipfs, identity, address, name, accessController, directory, meta, headsStorage, entryStorage, indexStorage, referencesCount, syncAutomatically }) => { +const Database = async ({ OpLog, ipfs, identity, address, name, access, directory, meta, headsStorage, entryStorage, indexStorage, referencesCount, syncAutomatically }) => { const { Log, Entry } = OpLog directory = pathJoin(directory || './orbitdb', `./${address}/`) @@ -29,7 +29,7 @@ const Database = async ({ OpLog, ipfs, identity, address, name, accessController await LevelStorage({ path: pathJoin(directory, '/log/_index/') }) ) - const log = await Log(identity, { logId: address, access: accessController, entryStorage, headsStorage, indexStorage }) + const log = await Log(identity, { logId: address, access, entryStorage, headsStorage, indexStorage }) const events = new EventEmitter() const queue = new PQueue({ concurrency: 1 }) @@ -88,7 +88,8 @@ const Database = async ({ OpLog, ipfs, identity, address, name, accessController log, sync, peers: sync.peers, - events + events, + access } } diff --git a/src/db/document-store.js b/src/db/document-store.js index 41e9420..bdc7b40 100644 --- a/src/db/document-store.js +++ b/src/db/document-store.js @@ -1,5 +1,5 @@ -const DocumentStore = async ({ OpLog, Database, ipfs, identity, address, name, accessController, directory, storage, meta, syncAutomatically, indexBy = '_id' }) => { - const database = await Database({ OpLog, ipfs, identity, address, name, accessController, directory, storage, meta, syncAutomatically }) +const DocumentStore = async ({ OpLog, Database, ipfs, identity, address, name, access, directory, storage, meta, syncAutomatically, indexBy = '_id' }) => { + const database = await Database({ OpLog, ipfs, identity, address, name, access, directory, storage, meta, syncAutomatically }) const { addOperation, log } = database diff --git a/src/db/event-store.js b/src/db/event-store.js index 2f24d63..89fd040 100644 --- a/src/db/event-store.js +++ b/src/db/event-store.js @@ -1,5 +1,5 @@ -const Events = async ({ OpLog, Database, ipfs, identity, address, name, accessController, directory, storage, meta, syncAutomatically }) => { - const database = await Database({ OpLog, ipfs, identity, address, name, accessController, directory, storage, meta, syncAutomatically }) +const Events = async ({ OpLog, Database, ipfs, identity, address, name, access, directory, storage, meta, syncAutomatically }) => { + const database = await Database({ OpLog, ipfs, identity, address, name, access, directory, storage, meta, syncAutomatically }) const { addOperation, log } = database diff --git a/src/db/keyvalue-persisted.js b/src/db/keyvalue-persisted.js index e487a1d..08c6bc5 100644 --- a/src/db/keyvalue-persisted.js +++ b/src/db/keyvalue-persisted.js @@ -5,8 +5,8 @@ import PQueue from 'p-queue' const valueEncoding = 'json' -const KeyValuePersisted = async ({ OpLog, Database, ipfs, identity, address, name, accessController, directory, storage, meta }) => { - const keyValueStore = await KeyValue({ OpLog, Database, ipfs, identity, address, name, accessController, directory, storage, meta }) +const KeyValuePersisted = async ({ OpLog, Database, ipfs, identity, address, name, access, directory, storage, meta }) => { + const keyValueStore = await KeyValue({ OpLog, Database, ipfs, identity, address, name, access, directory, storage, meta }) const { events, log } = keyValueStore const queue = new PQueue({ concurrency: 1 }) diff --git a/src/db/keyvalue.js b/src/db/keyvalue.js index 3987716..d39227e 100644 --- a/src/db/keyvalue.js +++ b/src/db/keyvalue.js @@ -1,5 +1,5 @@ -const KeyValue = async ({ OpLog, Database, ipfs, identity, address, name, accessController, directory, storage, meta, syncAutomatically }) => { - const database = await Database({ OpLog, ipfs, identity, address, name, accessController, directory, storage, meta, syncAutomatically }) +const KeyValue = async ({ OpLog, Database, ipfs, identity, address, name, access, directory, storage, meta, syncAutomatically }) => { + const database = await Database({ OpLog, ipfs, identity, address, name, access, directory, storage, meta, syncAutomatically }) const { addOperation, log } = database diff --git a/src/manifest.js b/src/manifest.js index 2925ba5..faf87b4 100644 --- a/src/manifest.js +++ b/src/manifest.js @@ -1,4 +1,3 @@ -import pathJoin from './utils/path-join.js' import * as Block from 'multiformats/block' import * as dagCbor from '@ipld/dag-cbor' import { sha256 } from 'multiformats/hashes/sha2' @@ -9,17 +8,17 @@ const hasher = sha256 const hashStringEncoding = base58btc // Creates a DB manifest file and saves it in IPFS -export default async (storage, name, type, accessControllerAddress, { meta } = {}) => { +export default async ({ storage, name, type, accessController, meta }) => { if (!storage) throw new Error('storage is required') if (!name) throw new Error('name is required') if (!type) throw new Error('type is required') - if (!accessControllerAddress) throw new Error('accessControllerAddress is required') + if (!accessController) throw new Error('accessController is required') const manifest = Object.assign( { name, type, - accessController: pathJoin('/ipfs', accessControllerAddress) + accessController }, // meta field is only added to manifest if meta parameter is defined meta !== undefined ? { meta } : {} diff --git a/src/utils/ensure-ac-address.js b/src/utils/ensure-ac-address.js new file mode 100644 index 0000000..c36593f --- /dev/null +++ b/src/utils/ensure-ac-address.js @@ -0,0 +1,9 @@ +import path from 'path' + +// Make sure the given address has '/_access' as the last part +export default address => { + const suffix = address.toString().split('/').pop() + return suffix === '_access' + ? address + : path.join(address, '/_access') +} diff --git a/test/access-controllers/access-controller-handlers.test.js b/test/access-controllers/access-controller-handlers.test.js index df8c73c..e922792 100644 --- a/test/access-controllers/access-controller-handlers.test.js +++ b/test/access-controllers/access-controller-handlers.test.js @@ -1,143 +1,138 @@ // import assert from 'assert' // import rmrf from 'rimraf' -// import Web3 from 'web3' +// // import Web3 from 'web3' // import OrbitDB from '../../src/OrbitDB.js' - -// import IdentityProvider from 'orbit-db-identity-provider' -// import Keystore from 'orbit-db-keystore' -// import AccessControllers from 'orbit-db-access-controllers' -// import ContractAccessController from 'orbit-db-access-controllers/contract' -// import ganache from 'ganache-cli' -// import Access from './Access.json' assert {type: "json"} - -// // Include test utilities -// import { -// config, -// startIpfs, -// stopIpfs, -// testAPIs -// } from 'orbit-db-test-utils' - +// +// // import IdentityProvider from 'orbit-db-identity-provider' +// import Keystore from '../../src/key-store.js' +// import AccessControllers from '../../src/access-controllers/index.js' +// // import ContractAccessController from 'orbit-db-access-controllers/contract' +// // import ganache from 'ganache-cli' +// // import Access from './Access.json' assert {type: "json"} +// import config from '../config.js' +// import connectPeers from '../utils/connect-nodes.js' +// // const abi = Access.abi // const bytecode = Access.bytecode // const dbPath1 = './orbitdb/tests/orbitdb-access-controller/1' // const dbPath2 = './orbitdb/tests/orbitdb-access-controller/2' - -// Object.keys(testAPIs).forEach(API => { -// describe(`orbit-db - Access Controller Handlers (${API})`, function () { -// this.timeout(config.timeout) - -// let web3, contract, ipfsd1, ipfsd2, ipfs1, ipfs2, id1, id2 -// let orbitdb1, orbitdb2 - -// before(async () => { -// rmrf.sync(dbPath1) -// rmrf.sync(dbPath2) -// ipfsd1 = await startIpfs(API, config.daemon1) -// ipfsd2 = await startIpfs(API, config.daemon2) -// ipfs1 = ipfsd1.api -// ipfs2 = ipfsd2.api - -// const keystore1 = new Keystore(dbPath1 + '/keys') -// const keystore2 = new Keystore(dbPath2 + '/keys') - -// id1 = await IdentityProvider.createIdentity({ id: 'A', keystore: keystore1 }) -// id2 = await IdentityProvider.createIdentity({ id: 'B', keystore: keystore2 }) - -// orbitdb1 = await OrbitDB.createInstance(ipfs1, { -// AccessControllers: AccessControllers, -// directory: dbPath1, -// identity: id1 -// }) - -// orbitdb2 = await OrbitDB.createInstance(ipfs2, { -// AccessControllers: AccessControllers, -// directory: dbPath2, -// identity: id2 -// }) +// +// describe('Access Controller Handlers', function () { +// this.timeout(config.timeout) +// +// let ipfs1, ipfs2 +// let orbitdb1, orbitdb2 +// +// before(async () => { +// ipfs1 = await IPFS.create({ ...config.daemon1, repo: './ipfs1' }) +// ipfs2 = await IPFS.create({ ...config.daemon2, repo: './ipfs2' }) +// await connectPeers(ipfs1, ipfs2) +// +// const keystore1 = await Keystore({ path: dbPath1 + '/keys' }) +// const keystore2 = await Keystore({ path: dbPath2 + '/keys' }) +// +// identities1 = await Identities({ keystore: keystore1 }) +// identities2 = await Identities({ keystore: keystore2 }) +// +// testIdentity1 = await identities1.createIdentity({ id: 'userA' }) +// testIdentity2 = await identities2.createIdentity({ id: 'userB' }) +// +// orbitdb1 = await OrbitDB({ ipfs: ipfs1, identity: testIdentity1, directory: dbPath1 }) +// orbitdb2 = await OrbitDB({ ipfs: ipfs2, identity: testIdentity2, directory: dbPath2 }) +// }) +// +// after(async () => { +// if (orbitdb1) { +// await orbitdb1.stop() +// } +// +// if (orbitdb2) { +// await orbitdb2.stop() +// } +// +// if (ipfs1) { +// await ipfs1.stop() +// } +// +// if (ipfs2) { +// await ipfs2.stop() +// } +// +// await rmrf('./orbitdb') +// await rmrf('./ipfs1') +// await rmrf('./ipfs2') +// }) +// +// describe.only('isSupported', function () { +// it('supports default access controllers', () => { +// assert.strictEqual(AccessControllers.isSupported('ipfs'), true) +// assert.strictEqual(AccessControllers.isSupported('orbitdb'), true) // }) - -// after(async () => { -// if (orbitdb1) { await orbitdb1.stop() } - -// if (orbitdb2) { await orbitdb2.stop() } - -// if (ipfsd1) { await stopIpfs(ipfsd1) } - -// if (ipfsd2) { await stopIpfs(ipfsd2) } -// }) - -// describe('isSupported', function () { -// it('supports default access controllers', () => { -// assert.strictEqual(AccessControllers.isSupported('ipfs'), true) -// assert.strictEqual(AccessControllers.isSupported('orbitdb'), true) -// }) - -// it('doesn\'t support smart contract access controller by default', () => { -// assert.strictEqual(AccessControllers.isSupported(ContractAccessController.type), false) -// }) -// }) - -// describe('addAccessController', function () { -// it('supports added access controller', () => { -// const options = { -// AccessController: ContractAccessController, -// web3: web3, -// abi: abi -// } -// AccessControllers.addAccessController(options) -// assert.strictEqual(AccessControllers.isSupported(ContractAccessController.type), true) -// }) -// }) - -// describe('create access controllers', function () { -// let options = { -// AccessController: ContractAccessController -// } - -// before(async () => { -// web3 = new Web3(ganache.provider()) -// const accounts = await web3.eth.getAccounts() -// contract = await new web3.eth.Contract(abi) -// .deploy({ data: bytecode }) -// .send({ from: accounts[0], gas: '1000000' }) -// options = Object.assign({}, options, { web3, abi, contractAddress: contract._address, defaultAccount: accounts[0] }) -// AccessControllers.addAccessController(options) -// }) - -// it('throws an error if AccessController is not defined', async () => { -// let err -// try { -// AccessControllers.addAccessController({}) -// } catch (e) { -// err = e.toString() -// } -// assert.strictEqual(err, 'Error: AccessController class needs to be given as an option') -// }) - -// it('throws an error if AccessController doesn\'t define type', async () => { -// let err -// try { -// AccessControllers.addAccessController({ AccessController: {} }) -// } catch (e) { -// err = e.toString() -// } -// assert.strictEqual(err, 'Error: Given AccessController class needs to implement: static get type() { /* return a string */}.') -// }) - -// it('creates a custom access controller', async () => { -// const type = ContractAccessController.type -// const acManifestHash = await AccessControllers.create(orbitdb1, type, options) -// assert.notStrictEqual(acManifestHash, null) - -// const ac = await AccessControllers.resolve(orbitdb1, acManifestHash, options) -// assert.strictEqual(ac.type, type) -// }) - -// it('removes the custom access controller', async () => { -// AccessControllers.removeAccessController(ContractAccessController.type) -// assert.strictEqual(AccessControllers.isSupported(ContractAccessController.type), false) -// }) +// +// it('doesn\'t support smart contract access controller by default', () => { +// assert.strictEqual(AccessControllers.isSupported(ContractAccessController.type), false) // }) // }) +// +// // describe('addAccessController', function () { +// // it('supports added access controller', () => { +// // const options = { +// // AccessController: ContractAccessController, +// // web3: web3, +// // abi: abi +// // } +// // AccessControllers.addAccessController(options) +// // assert.strictEqual(AccessControllers.isSupported(ContractAccessController.type), true) +// // }) +// // }) +// // +// // describe('create access controllers', function () { +// // let options = { +// // AccessController: ContractAccessController +// // } +// // +// // before(async () => { +// // web3 = new Web3(ganache.provider()) +// // const accounts = await web3.eth.getAccounts() +// // contract = await new web3.eth.Contract(abi) +// // .deploy({ data: bytecode }) +// // .send({ from: accounts[0], gas: '1000000' }) +// // options = Object.assign({}, options, { web3, abi, contractAddress: contract._address, defaultAccount: accounts[0] }) +// // AccessControllers.addAccessController(options) +// // }) +// // +// // it('throws an error if AccessController is not defined', async () => { +// // let err +// // try { +// // AccessControllers.addAccessController({}) +// // } catch (e) { +// // err = e.toString() +// // } +// // assert.strictEqual(err, 'Error: AccessController class needs to be given as an option') +// // }) +// // +// // it('throws an error if AccessController doesn\'t define type', async () => { +// // let err +// // try { +// // AccessControllers.addAccessController({ AccessController: {} }) +// // } catch (e) { +// // err = e.toString() +// // } +// // assert.strictEqual(err, 'Error: Given AccessController class needs to implement: static get type() { /* return a string */}.') +// // }) +// // +// // it('creates a custom access controller', async () => { +// // const type = ContractAccessController.type +// // const acManifestHash = await AccessControllers.create(orbitdb1, type, options) +// // assert.notStrictEqual(acManifestHash, null) +// // +// // const ac = await AccessControllers.resolve(orbitdb1, acManifestHash, options) +// // assert.strictEqual(ac.type, type) +// // }) +// // +// // it('removes the custom access controller', async () => { +// // AccessControllers.removeAccessController(ContractAccessController.type) +// // assert.strictEqual(AccessControllers.isSupported(ContractAccessController.type), false) +// // }) +// // }) // }) diff --git a/test/access-controllers/ipfs-access-controller.test.js b/test/access-controllers/ipfs-access-controller.test.js index 3a7e38e..01f74cd 100644 --- a/test/access-controllers/ipfs-access-controller.test.js +++ b/test/access-controllers/ipfs-access-controller.test.js @@ -17,6 +17,7 @@ describe('IPFSAccessController', function () { let keystore1, keystore2 let identities1, identities2 let testIdentity1, testIdentity2 + let orbitdb1, orbitdb2 before(async () => { ipfs1 = await IPFS.create({ ...config.daemon1, repo: './ipfs1' }) @@ -31,6 +32,9 @@ describe('IPFSAccessController', function () { testIdentity1 = await identities1.createIdentity({ id: 'userA' }) testIdentity2 = await identities2.createIdentity({ id: 'userB' }) + + orbitdb1 = { ipfs: ipfs1, identity: testIdentity1 } + orbitdb2 = { ipfs: ipfs2, identity: testIdentity2 } }) after(async () => { @@ -57,59 +61,72 @@ describe('IPFSAccessController', function () { let accessController - before(async () => { - accessController = await IPFSAccessController({ - ipfs: ipfs1, - identities: identities1, - identity: testIdentity1 + describe('Default write access', () => { + before(async () => { + accessController = await IPFSAccessController()({ + orbitdb: orbitdb1, + identities: identities1 + }) + }) + + it('creates an access controller', () => { + notStrictEqual(accessController, null) + notStrictEqual(accessController, undefined) + }) + + it('sets the controller type', () => { + strictEqual(accessController.type, 'ipfs') + }) + + it('sets default write', async () => { + deepStrictEqual(accessController.write, [testIdentity1.id]) + }) + + it('user with write access can append', async () => { + const mockEntry = { + identity: testIdentity1.hash, + v: 1 + // ... + // doesn't matter what we put here, only identity is used for the check + } + const canAppend = await accessController.canAppend(mockEntry) + strictEqual(canAppend, true) + }) + + it('user without write cannot append', async () => { + const mockEntry = { + identity: testIdentity2.hash, + v: 1 + // ... + // doesn't matter what we put here, only identity is used for the check + } + const canAppend = await accessController.canAppend(mockEntry) + strictEqual(canAppend, false) + }) + + it('replicates the access controller', async () => { + const replicatedAccessController = await IPFSAccessController()({ + orbitdb: orbitdb2, + identities: identities2, + address: accessController.address + }) + + strictEqual(replicatedAccessController.type, accessController.type) + strictEqual(replicatedAccessController.address, accessController.address) + deepStrictEqual(replicatedAccessController.write, accessController.write) }) }) - it('creates an access controller', () => { - notStrictEqual(accessController, null) - notStrictEqual(accessController, undefined) - }) - - it('sets the controller type', () => { - strictEqual(accessController.type, 'ipfs') - }) - - it('sets default write', async () => { - deepStrictEqual(accessController.write, [testIdentity1.id]) - }) - - it('user with write access can append', async () => { - const mockEntry = { - identity: testIdentity1.hash, - v: 1 - // ... - // doesn't matter what we put here, only identity is used for the check - } - const canAppend = await accessController.canAppend(mockEntry) - strictEqual(canAppend, true) - }) - - it('user without write cannot append', async () => { - const mockEntry = { - identity: testIdentity2.hash, - v: 1 - // ... - // doesn't matter what we put here, only identity is used for the check - } - const canAppend = await accessController.canAppend(mockEntry) - strictEqual(canAppend, false) - }) - - it('replicates the access controller', async () => { - const replicatedAccessController = await IPFSAccessController({ - ipfs: ipfs2, - identities: identities2, - identity: testIdentity2, - address: accessController.address + describe('Write all access', () => { + before(async () => { + accessController = await IPFSAccessController({ write: ['*'] })({ + orbitdb: orbitdb1, + identities: identities1 + }) }) - strictEqual(replicatedAccessController.type, accessController.type) - strictEqual(replicatedAccessController.address, accessController.address) - deepStrictEqual(replicatedAccessController.write, accessController.write) + it('sets write to \'Anyone\'', async () => { + deepStrictEqual(accessController.write, ['*']) + }) }) }) diff --git a/test/access-controllers/orbit-db-access-controller.test.js b/test/access-controllers/orbit-db-access-controller.test.js index a98d72b..df26908 100644 --- a/test/access-controllers/orbit-db-access-controller.test.js +++ b/test/access-controllers/orbit-db-access-controller.test.js @@ -1,336 +1,277 @@ -// import assert from 'assert' -// import rmrf from 'rimraf' -// import OrbitDB from '../../src/OrbitDB.js' -// import IdentityProvider from 'orbit-db-identity-provider' -// import Keystore from 'orbit-db-keystore' -// import OrbitDBAccessController from 'orbit-db-access-controllers/orbitdb' -// import AccessControllers from 'orbit-db-access-controllers' +import { strictEqual, deepStrictEqual, notStrictEqual } from 'assert' +import rmrf from 'rimraf' +import OrbitDB from '../../src/OrbitDB.js' +import * as IPFS from 'ipfs-core' +import Keystore from '../../src/key-store.js' +import Identities from '../../src/identities/identities.js' +import OrbitDBAccessController from '../../src/access-controllers/orbitdb.js' +import config from '../config.js' +import connectPeers from '../utils/connect-nodes.js' -// // Include test utilities -// import { -// config, -// startIpfs, -// stopIpfs, -// testAPIs -// } from 'orbit-db-test-utils' +const dbPath1 = './orbitdb/tests/orbitdb-access-controller/1' +const dbPath2 = './orbitdb/tests/orbitdb-access-controller/2' -// const dbPath1 = './orbitdb/tests/orbitdb-access-controller/1' -// const dbPath2 = './orbitdb/tests/orbitdb-access-controller/2' +describe('OrbitDBAccessController', function () { + this.timeout(config.timeout) -// Object.keys(testAPIs).forEach(API => { -// describe(`orbit-db - OrbitDBAccessController (${API})`, function () { -// this.timeout(config.timeout) + let ipfs1, ipfs2 + let orbitdb1, orbitdb2 + let identities1, identities2, testIdentity1, testIdentity2 -// let ipfsd1, ipfsd2, ipfs1, ipfs2, id1, id2 -// let orbitdb1, orbitdb2 + before(async () => { + ipfs1 = await IPFS.create({ ...config.daemon1, repo: './ipfs1' }) + ipfs2 = await IPFS.create({ ...config.daemon2, repo: './ipfs2' }) + await connectPeers(ipfs1, ipfs2) -// before(async () => { -// rmrf.sync(dbPath1) -// rmrf.sync(dbPath2) -// ipfsd1 = await startIpfs(API, config.daemon1) -// ipfsd2 = await startIpfs(API, config.daemon2) -// ipfs1 = ipfsd1.api -// ipfs2 = ipfsd2.api + const keystore1 = await Keystore({ path: dbPath1 + '/keys' }) + const keystore2 = await Keystore({ path: dbPath2 + '/keys' }) -// const keystore1 = new Keystore(dbPath1 + '/keys') -// const keystore2 = new Keystore(dbPath2 + '/keys') + identities1 = await Identities({ ipfs: ipfs1, keystore: keystore1 }) + identities2 = await Identities({ ipfs: ipfs2, keystore: keystore2 }) -// id1 = await IdentityProvider.createIdentity({ id: 'A', keystore: keystore1 }) -// id2 = await IdentityProvider.createIdentity({ id: 'B', keystore: keystore2 }) + testIdentity1 = await identities1.createIdentity({ id: 'userA' }) + testIdentity2 = await identities2.createIdentity({ id: 'userB' }) -// orbitdb1 = await OrbitDB.createInstance(ipfs1, { -// AccessControllers, -// directory: dbPath1, -// identity: id1 -// }) + orbitdb1 = await OrbitDB({ ipfs: ipfs1, identity: testIdentity1, directory: dbPath1, keystore: keystore1 }) + orbitdb2 = await OrbitDB({ ipfs: ipfs2, identity: testIdentity2, directory: dbPath2, keystore: keystore2 }) + }) -// orbitdb2 = await OrbitDB.createInstance(ipfs2, { -// AccessControllers, -// directory: dbPath2, -// identity: id2 -// }) -// }) + after(async () => { + if (orbitdb1) { + await orbitdb1.stop() + } -// after(async () => { -// if (orbitdb1) { -// await orbitdb1.stop() -// } + if (orbitdb2) { + await orbitdb2.stop() + } -// if (orbitdb2) { -// await orbitdb2.stop() -// } + if (ipfs1) { + await ipfs1.stop() + } -// if (ipfsd1) { -// await stopIpfs(ipfsd1) -// } + if (ipfs2) { + await ipfs2.stop() + } -// if (ipfsd2) { -// await stopIpfs(ipfsd2) -// } -// }) + await rmrf('./orbitdb') + await rmrf('./ipfs1') + await rmrf('./ipfs2') + }) -// describe('Constructor', function () { -// let accessController + describe('Default write access', function () { + let accessController -// before(async () => { -// accessController = await OrbitDBAccessController.create(orbitdb1) -// }) + before(async () => { + accessController = await OrbitDBAccessController()({ orbitdb: orbitdb1, identities: identities1 }) + }) -// it('creates an access controller', () => { -// assert.notStrictEqual(accessController, null) -// assert.notStrictEqual(accessController, undefined) -// }) + it('creates an access controller', () => { + notStrictEqual(accessController, null) + notStrictEqual(accessController, undefined) + }) -// it('sets the controller type', () => { -// assert.strictEqual(accessController.type, 'orbitdb') -// }) + it('sets the controller type', () => { + strictEqual(accessController.type, 'orbitdb') + }) -// it('has OrbitDB instance', async () => { -// assert.notStrictEqual(accessController._orbitdb, null) -// assert.strictEqual(accessController._orbitdb.id, orbitdb1.id) -// }) + it('sets default capabilities', async () => { + const expected = [] + expected.admin = new Set([testIdentity1.id]) -// it('has IPFS instance', async () => { -// const peerId1 = await accessController._orbitdb._ipfs.id() -// const peerId2 = await ipfs1.id() -// assert.strictEqual(String(peerId1.id), String(peerId2.id)) -// }) + deepStrictEqual(await accessController.capabilities(), expected) + }) -// it('sets default capabilities', async () => { -// assert.deepStrictEqual(accessController.capabilities, { -// admin: new Set([id1.id]) -// }) -// }) + it('allows owner to append after creation', async () => { + const mockEntry = { + identity: testIdentity1.hash + // ... + // doesn't matter what we put here, only identity is used for the check + } + const canAppend = await accessController.canAppend(mockEntry) + strictEqual(canAppend, true) + }) + }) -// it('allows owner to append after creation', async () => { -// const mockEntry = { -// identity: id1 -// // ... -// // doesn't matter what we put here, only identity is used for the check -// } -// const canAppend = await accessController.canAppend(mockEntry, id1.provider) -// assert.strictEqual(canAppend, true) -// }) -// }) + describe('grant', function () { + let accessController -// describe('grant', function () { -// let accessController + before(async () => { + accessController = await OrbitDBAccessController()({ orbitdb: orbitdb1, identities: identities1, address: 'testdb/add' }) + }) -// before(async () => { -// accessController = new OrbitDBAccessController(orbitdb1) -// await accessController.load('testdb/add') -// }) + it('adds a capability', async () => { + try { + await accessController.grant('write', testIdentity1.id) + } catch (e) { + strictEqual(e, null) + } -// it('loads the root access controller from IPFS', () => { -// assert.strictEqual(accessController._db.access.type, 'ipfs') -// assert.deepStrictEqual(accessController._db.access.write, [id1.id]) -// }) + const expected = [] + expected.admin = new Set([testIdentity1.id]) + expected.write = new Set([testIdentity1.id]) + deepStrictEqual(await accessController.capabilities(), expected) + }) -// it('adds a capability', async () => { -// try { -// await accessController.grant('write', id1.id) -// } catch (e) { -// assert(e, null) -// } -// assert.deepStrictEqual(accessController.capabilities, { -// admin: new Set([id1.id]), -// write: new Set([id1.id]) -// }) -// }) + it('adds more capabilities', async () => { + try { + await accessController.grant('read', 'ABCD') + await accessController.grant('delete', 'ABCD') + } catch (e) { + strictEqual(e, null) + } -// it('adds more capabilities', async () => { -// try { -// await accessController.grant('read', 'ABCD') -// await accessController.grant('delete', 'ABCD') -// } catch (e) { -// assert.strictEqual(e, null) -// } -// assert.deepStrictEqual(accessController.capabilities, { -// admin: new Set([id1.id]), -// write: new Set([id1.id]), -// read: new Set(['ABCD']), -// delete: new Set(['ABCD']) -// }) -// }) + const expected = [] + expected.admin = new Set([testIdentity1.id]) + expected.write = new Set([testIdentity1.id]) + expected.read = new Set(['ABCD']) + expected.delete = new Set(['ABCD']) -// it('emit \'updated\' event when a capability was added', async () => { -// return new Promise((resolve, reject) => { -// accessController.on('updated', () => { -// try { -// assert.deepStrictEqual(accessController.capabilities, { -// admin: new Set([id1.id]), -// write: new Set([id1.id]), -// read: new Set(['ABCD', 'AXES']), -// delete: new Set(['ABCD']) -// }) -// resolve() -// } catch (e) { -// reject(e) -// } -// }) -// accessController.grant('read', 'AXES') -// }) -// }) + deepStrictEqual(await accessController.capabilities(), expected) + }) -// it('can append after acquiring capability', async () => { -// try { -// await accessController.grant('write', id1.id) -// await accessController.grant('write', id2.id) -// } catch (e) { -// assert(e, null) -// } -// const mockEntry1 = { -// identity: id1 -// } -// const mockEntry2 = { -// identity: id2 -// } -// const canAppend1 = await accessController.canAppend(mockEntry1, id1.provider) -// const canAppend2 = await accessController.canAppend(mockEntry2, id2.provider) -// assert.strictEqual(canAppend1, true) -// assert.strictEqual(canAppend2, true) -// }) -// }) + it('emit \'update\' event when a capability was added', async () => { + let update = false + const onUpdate = (entry) => { + update = true + } -// describe('revoke', function () { -// let accessController + accessController.events.on('update', onUpdate) -// before(async () => { -// accessController = new OrbitDBAccessController(orbitdb1) -// await accessController.load('testdb/remove') -// }) + await accessController.grant('read', 'AXES') -// it('removes a capability', async () => { -// try { -// await accessController.grant('write', id1.id) -// await accessController.grant('write', 'AABB') -// await accessController.revoke('write', 'AABB') -// } catch (e) { -// assert.strictEqual(e, null) -// } -// assert.deepStrictEqual(accessController.capabilities, { -// admin: new Set([id1.id]), -// write: new Set([id1.id]) -// }) -// }) + strictEqual(update, true) + }) -// it('can remove the creator\'s write access', async () => { -// try { -// await accessController.revoke('write', id1.id) -// } catch (e) { -// assert.strictEqual(e, null) -// } -// assert.deepStrictEqual(accessController.capabilities, { -// admin: new Set([id1.id]) -// }) -// }) + it('can append after acquiring capability', async () => { + try { + await accessController.grant('write', testIdentity1.id) + await accessController.grant('write', testIdentity2.id) + } catch (e) { + strictEqual(e, null) + } -// it('can\'t remove the creator\'s admin access', async () => { -// try { -// await accessController.revoke('admin', id1.id) -// } catch (e) { -// assert.strictEqual(e, null) -// } -// assert.deepStrictEqual(accessController.capabilities, { -// admin: new Set([id1.id]) -// }) -// }) + const mockEntry1 = { + identity: testIdentity1.hash + } -// it('removes more capabilities', async () => { -// try { -// await accessController.grant('read', 'ABCD') -// await accessController.grant('delete', 'ABCD') -// await accessController.grant('write', id1.id) -// await accessController.revoke('read', 'ABCDE') -// await accessController.revoke('delete', 'ABCDE') -// } catch (e) { -// assert.strictEqual(e, null) -// } -// assert.deepStrictEqual(accessController.capabilities, { -// admin: new Set([id1.id]), -// delete: new Set(['ABCD']), -// read: new Set(['ABCD']), -// write: new Set([id1.id]) -// }) -// }) + const mockEntry2 = { + identity: testIdentity2.hash + } -// it('can\'t append after revoking capability', async () => { -// try { -// await accessController.grant('write', id2.id) -// await accessController.revoke('write', id2.id) -// } catch (e) { -// assert(e, null) -// } -// const mockEntry1 = { -// identity: id1 -// } -// const mockEntry2 = { -// identity: id2 -// } -// const canAppend = await accessController.canAppend(mockEntry1, id1.provider) -// const noAppend = await accessController.canAppend(mockEntry2, id2.provider) -// assert.strictEqual(canAppend, true) -// assert.strictEqual(noAppend, false) -// }) + const canAppend1 = await accessController.canAppend(mockEntry1) -// it('emits \'updated\' event when a capability was removed', async () => { -// await accessController.grant('admin', 'cats') -// await accessController.grant('admin', 'dogs') + const accessController2 = await OrbitDBAccessController()({ orbitdb: orbitdb2, identities: identities2, address: 'testdb/add' }) + const canAppend2 = await accessController2.canAppend(mockEntry2) -// return new Promise((resolve, reject) => { -// accessController.on('updated', () => { -// try { -// assert.deepStrictEqual(accessController.capabilities, { -// admin: new Set([id1.id, 'dogs']), -// delete: new Set(['ABCD']), -// read: new Set(['ABCD']), -// write: new Set([id1.id]) -// }) -// resolve() -// } catch (e) { -// reject(e) -// } -// }) -// accessController.revoke('admin', 'cats') -// }) -// }) -// }) + strictEqual(canAppend1, true) + strictEqual(canAppend2, true) + }) + }) -// describe('save and load', function () { -// let accessController, dbName + describe('revoke', function () { + let accessController -// before(async () => { -// dbName = 'testdb-load-' + new Date().getTime() -// accessController = new OrbitDBAccessController(orbitdb1) -// await accessController.load(dbName) -// await accessController.grant('write', 'A') -// await accessController.grant('write', 'B') -// await accessController.grant('write', 'C') -// await accessController.grant('write', 'C') // double entry -// await accessController.grant('another', 'AA') -// await accessController.grant('another', 'BB') -// await accessController.revoke('another', 'AA') -// await accessController.grant('admin', id1.id) -// return new Promise((resolve) => { -// // Test that the access controller emits 'updated' after it was loaded -// accessController.on('updated', () => resolve()) -// accessController.load(accessController.address) -// }) -// }) + before(async () => { + accessController = await OrbitDBAccessController()({ orbitdb: orbitdb1, identities: identities1, address: 'testdb/remove' }) + }) -// it('has the correct database address for the internal db', async () => { -// const addr = accessController._db.address.toString().split('/') -// assert.strictEqual(addr[addr.length - 1], '_access') -// assert.strictEqual(addr[addr.length - 2], dbName) -// }) + it('removes a capability', async () => { + try { + await accessController.grant('write', testIdentity1.id) + await accessController.grant('write', 'AABB') + await accessController.revoke('write', 'AABB') + } catch (e) { + strictEqual(e, null) + } -// it('has correct capabalities', async () => { -// assert.deepStrictEqual(accessController.get('admin'), new Set([id1.id])) -// assert.deepStrictEqual(accessController.get('write'), new Set(['A', 'B', 'C'])) -// assert.deepStrictEqual(accessController.get('another'), new Set(['BB'])) -// }) -// }) -// }) -// // TODO: use two separate peers for testing the AC -// // TODO: add tests for revocation correctness with a database (integration tests) -// }) + const expected = [] + expected.admin = new Set([testIdentity1.id]) + expected.write = new Set([testIdentity1.id]) + + deepStrictEqual(await accessController.capabilities(), expected) + }) + + it('can remove the creator\'s write access', async () => { + try { + await accessController.revoke('write', testIdentity1.id) + } catch (e) { + strictEqual(e, null) + } + + const expected = [] + expected.admin = new Set([testIdentity1.id]) + + deepStrictEqual(await accessController.capabilities(), expected) + }) + + it('can\'t remove the creator\'s admin access', async () => { + try { + await accessController.revoke('admin', testIdentity1.id) + } catch (e) { + strictEqual(e, null) + } + + const expected = [] + expected.admin = new Set([testIdentity1.id]) + + deepStrictEqual(await accessController.capabilities(), expected) + }) + + it('removes more capabilities', async () => { + try { + await accessController.grant('read', 'ABCD') + await accessController.grant('delete', 'ABCD') + await accessController.grant('write', testIdentity1.id) + await accessController.revoke('read', 'ABCDE') + await accessController.revoke('delete', 'ABCDE') + } catch (e) { + strictEqual(e, null) + } + + const expected = [] + expected.admin = new Set([testIdentity1.id]) + expected.write = new Set([testIdentity1.id]) + expected.read = new Set(['ABCD']) + expected.delete = new Set(['ABCD']) + + deepStrictEqual(await accessController.capabilities(), expected) + }) + + it('can\'t append after revoking capability', async () => { + try { + await accessController.grant('write', testIdentity2.id) + await accessController.revoke('write', testIdentity2.id) + } catch (e) { + strictEqual(e, null) + } + const mockEntry1 = { + identity: testIdentity1.hash + } + const mockEntry2 = { + identity: testIdentity2.hash + } + const canAppend = await accessController.canAppend(mockEntry1) + const noAppend = await accessController.canAppend(mockEntry2) + strictEqual(canAppend, true) + strictEqual(noAppend, false) + }) + + it('emits \'update\' event when a capability was removed', async () => { + await accessController.grant('admin', 'cats') + await accessController.grant('admin', 'dogs') + + let update = false + const onUpdate = (entry) => { + update = true + } + + accessController.events.on('update', onUpdate) + + await accessController.revoke('admin', 'cats') + + strictEqual(update, true) + }) + }) +}) +// TODO: use two separate peers for testing the AC +// TODO: add tests for revocation correctness with a database (integration tests) diff --git a/test/manifest.test.js b/test/manifest.test.js index 17c3fb4..934628e 100644 --- a/test/manifest.test.js +++ b/test/manifest.test.js @@ -1,5 +1,4 @@ import { strictEqual, deepStrictEqual } from 'assert' -import path from 'path' import rmrf from 'rimraf' import * as IPFS from 'ipfs-core' import Manifest from '../src/manifest.js' @@ -24,16 +23,16 @@ describe('Manifest', () => { it('creates a manifest', async () => { const name = 'manifest' - const type = 'manifest-test' - const accessController = '123' - const expectedHash = 'zdpuAtUvd7EhN9Xu2KSCxkjG1oS1SN6EnnZ8sxvJMPiJhbQWF' + const type = 'manifest' + const accessController = 'test/default-access-controller' + const expectedHash = 'zdpuAx3LaygjPHa2zsUmRoR4jQPm2WYrExsvz2gncfm62aRKv' const expectedManifest = { name, type, - accessController: path.join('/ipfs', accessController) + accessController } - const { hash, manifest } = await Manifest(storage, name, type, accessController) + const { hash, manifest } = await Manifest({ storage, name, type, accessController }) strictEqual(hash, expectedHash) deepStrictEqual(manifest, expectedManifest) @@ -41,12 +40,12 @@ describe('Manifest', () => { it('creates a manifest with metadata', async () => { const name = 'manifest' - const type = 'manifest-test' - const accessController = '123' - const expectedHash = 'zdpuAmNAMNnzKJ2kWgo4H42ZDG7nFCSGEWtV76UvL5dWrNweQ' - const meta = { name, type, description: 'more information about the database' } + const type = 'manifest' + const accessController = 'test/default-access-controller' + const expectedHash = 'zdpuAmegd2PpDfTQRVhGiATCkWQDvp3JygT9WksWgJkG2u313' + const meta = { name, description: 'more information about the database' } - const { hash, manifest } = await Manifest(storage, name, type, accessController, { meta }) + const { hash, manifest } = await Manifest({ storage, name, type, accessController, meta }) strictEqual(hash, expectedHash) deepStrictEqual(manifest.meta, meta) @@ -56,7 +55,7 @@ describe('Manifest', () => { let err try { - await Manifest() + await Manifest({}) } catch (e) { err = e.toString() } @@ -68,7 +67,7 @@ describe('Manifest', () => { let err try { - await Manifest(storage) + await Manifest({ storage }) } catch (e) { err = e.toString() } @@ -80,7 +79,7 @@ describe('Manifest', () => { let err try { - await Manifest(storage, 'manifest') + await Manifest({ storage, name: 'manifest' }) } catch (e) { err = e.toString() } @@ -88,15 +87,15 @@ describe('Manifest', () => { strictEqual(err, 'Error: type is required') }) - it('throws an error if accessControllerAddress is not specified', async () => { + it('throws an error if accessController is not specified', async () => { let err try { - await Manifest(storage, 'manifest', 'manifest-test') + await Manifest({ storage, name: 'manifest', type: 'manifest' }) } catch (e) { err = e.toString() } - strictEqual(err, 'Error: accessControllerAddress is required') + strictEqual(err, 'Error: accessController is required') }) }) diff --git a/test/orbitdb-access-controllers.test.js b/test/orbitdb-access-controllers.test.js new file mode 100644 index 0000000..dcf6b5e --- /dev/null +++ b/test/orbitdb-access-controllers.test.js @@ -0,0 +1,115 @@ +import { strictEqual, deepStrictEqual, notStrictEqual } from 'assert' +import rmrf from 'rimraf' +import * as IPFS from 'ipfs-core' +import OrbitDB, { AccessControllers } from '../src/OrbitDB.js' +import config from './config.js' +import pathJoin from '../src/utils/path-join.js' + +const type = 'custom!' + +const CustomAccessController = () => async ({ orbitdb, identities, address }) => { + address = pathJoin('/', type, 'controller') + + return { + address + } +} + +CustomAccessController.type = type + +describe('Add a custom access controller', function () { + this.timeout(5000) + + let ipfs + let orbitdb + + before(async () => { + ipfs = await IPFS.create({ ...config.daemon1, repo: './ipfs1' }) + orbitdb = await OrbitDB({ ipfs }) + }) + + after(async () => { + if (orbitdb) { + await orbitdb.stop() + } + + if (ipfs) { + await ipfs.stop() + } + + // Remove the added custom database type from OrbitDB import + AccessControllers.remove(type) + + await rmrf('./orbitdb') + await rmrf('./ipfs1') + }) + + describe('Default supported access controllers', function () { + it('returns default supported access controllers', async () => { + const expected = [ + 'ipfs', + 'orbitdb' + ] + + deepStrictEqual(Object.keys(AccessControllers.types), expected) + }) + + it('throws and error if custom access controller hasn\'t been added', async () => { + let err + try { + const db = await orbitdb.open('hello', { AccessController: CustomAccessController() }) + + await db.close() + await orbitdb.open(db.address) + } catch (e) { + err = e + } + notStrictEqual(err, undefined) + strictEqual(err.message, 'AccessController type \'custom!\' is not supported') + }) + }) + + describe('Custom access controller', function () { + before(() => { + AccessControllers.add(CustomAccessController) + }) + + it('create a database with the custom access controller', async () => { + const name = 'hello custom AC' + const db = await orbitdb.open(name, { AccessController: CustomAccessController() }) + strictEqual(db.access.address, '/custom!/controller') + }) + + it('throws and error if custom access controller already exists', async () => { + let err + try { + AccessControllers.add(CustomAccessController) + } catch (e) { + err = e.toString() + } + + strictEqual(err, 'Error: Access controller \'custom!\' already added.') + }) + + it('returns custom access controller after adding it', async () => { + const expected = [ + 'ipfs', + 'orbitdb', + type + ] + + deepStrictEqual(Object.keys(AccessControllers.types), expected) + }) + + it('can be removed from supported access controllers', async () => { + const expected = [ + 'ipfs', + 'orbitdb' + ] + + AccessControllers.remove(type) + + deepStrictEqual(Object.keys(AccessControllers.types), expected) + }) + }) +}) diff --git a/test/orbitdb-custom-database-types.test.js b/test/orbitdb-custom-database-types.test.js index 4afede0..a2c57b1 100644 --- a/test/orbitdb-custom-database-types.test.js +++ b/test/orbitdb-custom-database-types.test.js @@ -61,7 +61,7 @@ describe('Add a custom database type', function () { err = e } notStrictEqual(err, undefined) - strictEqual(err.message, 'Unspported database type: \'custom!\'') + strictEqual(err.message, 'Unsupported database type: \'custom!\'') }) }) diff --git a/test/orbitdb-write-access.test.js b/test/orbitdb-write-access.test.js index 0036fe4..7715407 100644 --- a/test/orbitdb-write-access.test.js +++ b/test/orbitdb-write-access.test.js @@ -6,6 +6,8 @@ import OrbitDB from '../src/OrbitDB.js' import config from './config.js' import waitFor from './utils/wait-for.js' import connectPeers from './utils/connect-nodes.js' +import IPFSAccessController from '../src/access-controllers/ipfs.js' +import OrbitDBAccessController from '../src/access-controllers/orbitdb.js' const dbPath = './orbitdb/tests/write-permissions' @@ -46,7 +48,7 @@ describe('Write Permissions', function () { await rmrf('./ipfs2') }) - it('throws an error if a peer writes to a log with default write access', async () => { + it('throws an error if another peer writes to a log with default write access', async () => { let err let connected = false @@ -87,7 +89,7 @@ describe('Write Permissions', function () { ++updateCount } - const db1 = await orbitdb1.open('write-test', { write: ['*'] }) + const db1 = await orbitdb1.open('write-test', { AccessController: IPFSAccessController({ write: ['*'] }) }) const db2 = await orbitdb2.open(db1.address) db2.events.on('join', onConnected) @@ -111,13 +113,14 @@ describe('Write Permissions', function () { let updateCount = 0 const options = { - // Set write access for both clients - write: [ - orbitdb1.identity.id, - orbitdb2.identity.id - ] + AccessController: IPFSAccessController({ + // Set write access for both clients + write: [ + orbitdb1.identity.id, + orbitdb2.identity.id + ] + }) } - const onConnected = async (peerId, heads) => { connected = true } @@ -150,9 +153,11 @@ describe('Write Permissions', function () { let connected = false const options = { - write: [ - orbitdb1.identity.id - ] + AccessController: IPFSAccessController({ + write: [ + orbitdb1.identity.id + ] + }) } const onConnected = async (peerId, heads) => { @@ -179,4 +184,38 @@ describe('Write Permissions', function () { await db1.close() await db2.close() }) + + it('uses an OrbitDB access controller to manage access', async () => { + let connected = false + let updateCount = 0 + + const onConnected = async (peerId, heads) => { + connected = true + } + + const onUpdate = async (entry) => { + ++updateCount + } + + const db1 = await orbitdb1.open('write-test', { AccessController: OrbitDBAccessController() }) + const db2 = await orbitdb2.open(db1.address, { AccessController: OrbitDBAccessController() }) + + db2.events.on('join', onConnected) + db2.events.on('update', onUpdate) + + await waitFor(() => connected, () => true) + + await db1.access.grant('write', db2.identity.id) + await db2.access.grant('write', db1.identity.id) + + await db1.add('record 1') + await db2.add('record 2') + + await waitFor(() => updateCount === 2, () => true) + + strictEqual((await db1.all()).length, (await db2.all()).length) + + await db1.close() + await db2.close() + }) })