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 6 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
37 changes: 35 additions & 2 deletions add-on/src/contentScripts/ipfs-proxy/page.js
Original file line number Diff line number Diff line change
@@ -1,7 +1,40 @@
'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) => {
// (This should be a lazy-load)
// 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)
// 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()
assign(proxyClient, createEnableCommand(proxyClient))
// TODO: return thin object with lazy-init inside of window.ipfs.enable
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
54 changes: 54 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,54 @@
const { inCommandWhitelist, createCommandWhitelistError } = require('./pre-command')
const { inNoAclPromptWhitelist, 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
if (!inNoAclPromptWhitelist(command)) {
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
61 changes: 49 additions & 12 deletions add-on/src/lib/ipfs-proxy/pre-acl.js
Original file line number Diff line number Diff line change
Expand Up @@ -8,27 +8,64 @@ const ACL_WHITELIST = Object.freeze(require('./acl-whitelist.json'))
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')
if (!getState().ipfsProxy) throw new Error('User disabled access to API proxy in IPFS Companion')

// No need to verify access if permission is on the whitelist
if (ACL_WHITELIST.includes(permission)) return args
if (inNoAclPromptWhitelist(permission)) return args

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
function inNoAclPromptWhitelist (permission) {
return ACL_WHITELIST.includes(permission)
}

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 is not on the ACL_WHITELIST
// 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,
inNoAclPromptWhitelist,
ACL_WHITELIST
}
24 changes: 0 additions & 24 deletions add-on/src/lib/ipfs-proxy/pre-api-whitelist.js

This file was deleted.

55 changes: 55 additions & 0 deletions add-on/src/lib/ipfs-proxy/pre-command.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
// Some API commands are too sensitive to be exposed to dapps on every website
// We follow a safe security practice of denying everything and allowing access
// to a pre-approved list of known APIs. This way if a new API is added
// it will be blocked by default, until it is explicitly enabled below.
const COMMAND_WHITELIST = Object.freeze(require('./command-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 due to API not being whitelisted
function createPreCommand (permission) {
return async (...args) => {
if (!inCommandWhitelist(permission)) {
throw createCommandWhitelistError(permission)
}
return args
}
}

function inCommandWhitelist (permission) {
// Fail fast if API or namespace is not explicitly whitelisted
const permRoot = permission.split('.')[0]
return COMMAND_WHITELIST.includes(permRoot) || COMMAND_WHITELIST.includes(permission)
}

// Standardized error thrown when a command is not on the COMMAND_WHITELIST
// TODO: return errors following conventions from https://github.com/ipfs/js-ipfs/pull/1746
function createCommandWhitelistError (permission) {
const permissions = Array.isArray(permission) ? permission : [permission]
console.warn(`[ipfs-companion] Access to '${permission}' commands over window.ipfs is blocked. If you feel it should be allowed, open an issue at https://github.com/ipfs-shipyard/ipfs-companion/issues/new`)
const err = new Error(`Access to '${permission}' commands over IPFS Proxy is globally blocked`)
err.output = {
payload: {
// Error follows convention from https://github.com/ipfs/js-ipfs/pull/1746/files
code: 'ERR_IPFS_PROXY_ACCESS_DENIED',
permissions,
// TODO: remove below after deprecation period ends with Q1
get isIpfsProxyWhitelistError () {
console.warn("[ipfs-companion] reading .isIpfsProxyWhitelistError 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 = {
createPreCommand,
createCommandWhitelistError,
inCommandWhitelist,
COMMAND_WHITELIST
}
Loading