Skip to content

Commit

Permalink
Merge pull request #619 from ipfs-shipyard/feat/window.ipfs-2.0
Browse files Browse the repository at this point in the history
Add window.ipfs.enable(opts) (Bulk Permission Prompt)
  • Loading branch information
lidel committed Jan 7, 2019
2 parents 4446839 + f3e9c6c commit 978bb33
Show file tree
Hide file tree
Showing 25 changed files with 3,185 additions and 1,342 deletions.
4 changes: 2 additions & 2 deletions add-on/_locales/en/messages.json
Original file line number Diff line number Diff line change
Expand Up @@ -464,11 +464,11 @@
"description": "Button title for revoking a permission (page_proxyAcl_revoke_button_title)"
},
"page_proxyAccessDialog_title": {
"message": "Allow $1 to access ipfs.$2?",
"message": "Should IPFS Companion allow «$1» to access «$2» at the connected node?",
"description": "Main title of the access permission dialog (page_proxyAccessDialog_title)"
},
"page_proxyAccessDialog_wildcardCheckbox_label": {
"message": "Apply this decision to all permissions in this scope",
"message": "Apply this decision to all current and future permissions in this scope",
"description": "Label for the apply permissions to all checkbox (page_proxyAccessDialog_wildcardCheckbox_label)"
},
"page_proxyAcl_revoke_all_button_title": {
Expand Down
49 changes: 47 additions & 2 deletions add-on/src/contentScripts/ipfs-proxy/page.js
Original file line number Diff line number Diff line change
@@ -1,7 +1,52 @@
'use strict'

const { createProxyClient } = require('ipfs-postmsg-proxy')
const _Buffer = Buffer
const { assign, freeze } = Object

// TODO: (wip) this should not be injected by default into every page,
// instead should be lazy-loaded when .enable() method is called for the first time
const { createProxyClient } = require('ipfs-postmsg-proxy')

function createEnableCommand (proxyClient) {
return {
enable: async (opts) => {
// Send message to proxy server for additional validation
// eg. trigger user prompt if a list of requested capabilities is not empty
// or fail fast and throw if IPFS Proxy is disabled globally
await require('postmsg-rpc').call('proxy.enable', opts)
// Create client
const proxyClient = createProxyClient()
// Additional client-side features
if (opts && opts.experiments) {
if (opts.experiments.ipfsx) {
// Experiment: wrap API with https://github.com/alanshaw/ipfsx
return freeze(require('ipfsx')(proxyClient))
}
}
return freeze(proxyClient)
}
}
}

function createWindowIpfs () {
const proxyClient = createProxyClient()

// Add deprecation warning to window.ipfs.<cmd>
for (let cmd in proxyClient) {
let fn = proxyClient[cmd]
proxyClient[cmd] = function () {
console.warn('Calling commands directly on window.ipfs is deprecated and will be removed on 2019-04-01. Use API instance returned by window.ipfs.enable() instead. More: https://github.com/ipfs-shipyard/ipfs-companion/blob/master/docs/window.ipfs.md')
return fn.apply(this, arguments)
}
}

// TODO: return thin object with lazy-init inside of window.ipfs.enable
assign(proxyClient, createEnableCommand())

return freeze(proxyClient)
}

// TODO: we should remove Buffer and add support for Uint8Array/ArrayBuffer natively
// See: https://github.com/ipfs/interface-ipfs-core/issues/404
window.Buffer = window.Buffer || _Buffer
window.ipfs = window.ipfs || createProxyClient()
window.ipfs = window.ipfs || createWindowIpfs()
23 changes: 14 additions & 9 deletions add-on/src/lib/ipfs-proxy/access-control.js
Original file line number Diff line number Diff line change
Expand Up @@ -78,6 +78,7 @@ class AccessControl extends EventEmitter {
)
}

// Return current access rights to given permission.
async getAccess (scope, permission) {
if (!isScope(scope)) throw new TypeError('Invalid scope')
if (!isString(permission)) throw new TypeError('Invalid permission')
Expand All @@ -101,42 +102,45 @@ class AccessControl extends EventEmitter {
}
}

return allow == null ? null : { scope: matchingScope, permission, allow }
return allow == null ? null : { scope: matchingScope, permissions: [permission], allow }
}

async setAccess (scope, permission, allow) {
// Set access rights to given permissions.
// 'permissions' can be an array of strings or a single string
async setAccess (scope, permissions, allow) {
permissions = Array.isArray(permissions) ? permissions : [permissions]
if (!isScope(scope)) throw new TypeError('Invalid scope')
if (!isString(permission)) throw new TypeError('Invalid permission')
if (!isStringArray(permissions)) throw new TypeError('Invalid permissions')
if (!isBoolean(allow)) throw new TypeError('Invalid allow')

return this._writeQ.add(async () => {
const allAccess = await this._getAllAccess(scope)

// Trying to set access for non-wildcard permission, when wildcard
// permission is already granted?
if (allAccess.has('*') && permission !== '*') {
if (allAccess.has('*') && !permissions.includes('*')) {
if (allAccess.get('*') === allow) {
// Noop if requested access is the same as access for wildcard grant
return { scope, permission, allow }
return { scope, permissions, allow }
} else {
// Fail if requested access is the different to access for wildcard grant
throw new Error(`Illegal set access for ${permission} when wildcard exists`)
throw new Error(`Illegal set access for '${permissions}' when wildcard exists`)
}
}

// If setting a wildcard permission, remove existing grants
if (permission === '*') {
if (permissions.includes('*')) {
allAccess.clear()
}

allAccess.set(permission, allow)
permissions.forEach(permission => allAccess.set(permission, allow))

const accessKey = this._getAccessKey(scope)
await this._storage.local.set({ [accessKey]: JSON.stringify(Array.from(allAccess)) })

await this._addScope(scope)

return { scope, permission, allow }
return { scope, permissions, allow }
})
}

Expand Down Expand Up @@ -199,4 +203,5 @@ const isScope = (value) => {
}

const isString = (value) => Object.prototype.toString.call(value) === '[object String]'
const isStringArray = (value) => Array.isArray(value) && value.length && value.every(isString)
const isBoolean = (value) => value === true || value === false
25 changes: 0 additions & 25 deletions add-on/src/lib/ipfs-proxy/acl-whitelist.json

This file was deleted.

52 changes: 52 additions & 0 deletions add-on/src/lib/ipfs-proxy/enable-command.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
const { inCommandWhitelist, createCommandWhitelistError } = require('./pre-command')
const { createProxyAclError } = require('./pre-acl')

// Artificial API command responsible for backend orchestration
// during window.ipfs.enable()
function createEnableCommand (getIpfs, getState, getScope, accessControl, requestAccess) {
return async (opts) => {
const scope = await getScope()
console.log(`[ipfs-companion] received window.ipfs.enable request from ${scope}`, opts)

// Check if all access to the IPFS node is disabled
if (!getState().ipfsProxy) throw new Error('User disabled access to API proxy in IPFS Companion')

// NOOP if .enable() was called without any arguments
if (!opts) return

// Validate and prompt for any missing permissions in bulk
// if a list of needed commands is announced up front
if (opts.commands) {
let missingAcls = []
let deniedAcls = []
for (let command of opts.commands) {
// Fail fast if command is not allowed to be proxied at all
if (!inCommandWhitelist(command)) {
throw createCommandWhitelistError(command)
}
// Get the current access flag to decide if it should be added
// to the list of permissions to be prompted about in the next step
let access = await accessControl.getAccess(scope, command)
if (!access) {
missingAcls.push(command)
} else if (access.allow !== true) {
deniedAcls.push(command)
}
}
// Fail fast if user already denied any of requested permissions
if (deniedAcls.length) {
throw createProxyAclError(scope, deniedAcls)
}
// Display a single prompt with all missing permissions
if (missingAcls.length) {
const { allow, wildcard } = await requestAccess(scope, missingAcls)
let access = await accessControl.setAccess(scope, wildcard ? '*' : missingAcls, allow)
if (!access.allow) {
throw createProxyAclError(scope, missingAcls)
}
}
}
}
}

module.exports = createEnableCommand
20 changes: 16 additions & 4 deletions add-on/src/lib/ipfs-proxy/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,11 @@

const browser = require('webextension-polyfill')
const { createProxyServer, closeProxyServer } = require('ipfs-postmsg-proxy')
const { expose } = require('postmsg-rpc')
const AccessControl = require('./access-control')
const createPreApiWhitelist = require('./pre-api-whitelist')
const createPreAcl = require('./pre-acl')
const createEnableCommand = require('./enable-command')
const { createPreCommand } = require('./pre-command')
const { createPreAcl } = require('./pre-acl')
const createPreMfsScope = require('./pre-mfs-scope')
const createRequestAccess = require('./request-access')

Expand All @@ -28,16 +30,26 @@ function createIpfsProxy (getIpfs, getState) {
return origin + pathname
}

const proxy = createProxyServer(getIpfs, {
// https://github.com/ipfs-shipyard/ipfs-postmsg-proxy#api
const proxyCfg = {
addListener: (_, handler) => port.onMessage.addListener(handler),
removeListener: (_, handler) => port.onMessage.removeListener(handler),
postMessage: (data) => port.postMessage(data),
getMessageData: (d) => d,
pre: (fnName) => [
createPreApiWhitelist(fnName),
createPreCommand(fnName),
createPreAcl(fnName, getState, getScope, accessControl, requestAccess),
createPreMfsScope(fnName, getScope, getIpfs)
]
}

const proxy = createProxyServer(getIpfs, proxyCfg)

// Extend proxy with Companion-specific commands:
const enableCommand = createEnableCommand(getIpfs, getState, getScope, accessControl, requestAccess)
Object.assign(proxy, {
// window.ipfs.enable(opts)
'proxy.enable': expose('proxy.enable', enableCommand, proxyCfg)
})

const close = () => {
Expand Down
60 changes: 42 additions & 18 deletions add-on/src/lib/ipfs-proxy/pre-acl.js
Original file line number Diff line number Diff line change
@@ -1,34 +1,58 @@
// This are the functions that DO NOT require an allow/deny decision by the user.
// All other IPFS functions require authorization.
const ACL_WHITELIST = Object.freeze(require('./acl-whitelist.json'))

// Creates a "pre" function that is called prior to calling a real function
// on the IPFS instance. It will throw if access is denied, and ask the user if
// no access decision has been made yet.
function createPreAcl (permission, getState, getScope, accessControl, requestAccess) {
return async (...args) => {
// Check if all access to the IPFS node is disabled
if (!getState().ipfsProxy) throw new Error('User disabled access to IPFS')

// No need to verify access if permission is on the whitelist
if (ACL_WHITELIST.includes(permission)) return args
if (!getState().ipfsProxy) throw new Error('User disabled access to API proxy in IPFS Companion')

const scope = await getScope()
let access = await accessControl.getAccess(scope, permission)

if (!access) {
const { allow, wildcard } = await requestAccess(scope, permission)
access = await accessControl.setAccess(scope, wildcard ? '*' : permission, allow)
}
const access = await getAccessWithPrompt(accessControl, requestAccess, scope, permission)

if (!access.allow) {
const err = new Error(`User denied access to ${permission}`)
err.output = { payload: { isIpfsProxyAclError: true, permission, scope } }
throw err
throw createProxyAclError(scope, permission)
}

return args
}
}

module.exports = createPreAcl
async function getAccessWithPrompt (accessControl, requestAccess, scope, permission) {
let access = await accessControl.getAccess(scope, permission)
if (!access) {
const { allow, wildcard } = await requestAccess(scope, permission)
access = await accessControl.setAccess(scope, wildcard ? '*' : permission, allow)
}
return access
}

// Standardized error thrown when a command access is denied
// TODO: return errors following conventions from https://github.com/ipfs/js-ipfs/pull/1746
function createProxyAclError (scope, permission) {
const err = new Error(`User denied access to selected commands over IPFS proxy: ${permission}`)
const permissions = Array.isArray(permission) ? permission : [permission]
err.output = {
payload: {
// Error follows convention from https://github.com/ipfs/js-ipfs/pull/1746/files
code: 'ERR_IPFS_PROXY_ACCESS_DENIED',
permissions,
scope,
// TODO: remove below after deprecation period ends with Q1
get isIpfsProxyAclError () {
console.warn("[ipfs-companion] reading .isIpfsProxyAclError from Ipfs Proxy errors is deprecated, use '.code' instead")
return true
},
get permission () {
if (!this.permissions || !this.permissions.length) return undefined
console.warn("[ipfs-companion] reading .permission from Ipfs Proxy errors is deprecated, use '.permissions' instead")
return this.permissions[0]
}
}
}
return err
}

module.exports = {
createPreAcl,
createProxyAclError
}
24 changes: 0 additions & 24 deletions add-on/src/lib/ipfs-proxy/pre-api-whitelist.js

This file was deleted.

Loading

0 comments on commit 978bb33

Please sign in to comment.