Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add window.ipfs.enable(opts) (Bulk Permission Prompt) #619

Merged
merged 9 commits into from
Jan 7, 2019
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions add-on/_locales/en/messages.json
Original file line number Diff line number Diff line change
Expand Up @@ -460,11 +460,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) {
Copy link
Member Author

@lidel lidel Dec 16, 2018

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The idea is to enable opt-in experiments such as ipfsx which is used as an example:

let ipfsx = await window.ipfs.enable({
  commands: ['add','files.addPullStream'],
  experiments: { ipfsx: true }
})

// 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