Skip to content

Commit

Permalink
Merge pull request #386 from tableflip/feat/scoped-permissions
Browse files Browse the repository at this point in the history
Adds scoped permissions
  • Loading branch information
lidel authored Feb 20, 2018
2 parents 4af391c + dcbd868 commit a5061d3
Show file tree
Hide file tree
Showing 15 changed files with 736 additions and 222 deletions.
25 changes: 10 additions & 15 deletions add-on/_locales/en/messages.json
Original file line number Diff line number Diff line change
Expand Up @@ -355,22 +355,22 @@
"description": "Message displayed when no permissions have been granted (page_proxyAcl_no_perms)"
},
"page_proxyAcl_confirm_revoke": {
"message": "Revoke permission $PERMISSION$ for $ORIGIN$?",
"description": "Confirmation message for revoking a permission for an origin (page_proxyAcl_confirm_revoke)",
"message": "Revoke permission $PERMISSION$ for $SCOPE$?",
"description": "Confirmation message for revoking a permission for a scope (page_proxyAcl_confirm_revoke)",
"placeholders": {
"permission": {
"content": "$1"
},
"origin": {
"scope": {
"content": "$2"
}
}
},
"page_proxyAcl_confirm_revoke_all": {
"message": "Revoke all permissions for $ORIGIN$?",
"description": "Confirmation message for revoking all permissions for an origin (page_proxyAcl_confirm_revoke_all)",
"message": "Revoke all permissions for $SCOPE$?",
"description": "Confirmation message for revoking all permissions for an scope (page_proxyAcl_confirm_revoke_all)",
"placeholders": {
"origin": {
"scope": {
"content": "$1"
}
}
Expand Down Expand Up @@ -401,10 +401,10 @@
}
},
"page_proxyAccessDialog_title": {
"message": "Allow $ORIGIN$ to access ipfs.$PERMISSION$?",
"message": "Allow $SCOPE$ to access ipfs.$PERMISSION$?",
"description": "Main title of the access permission dialog (page_proxyAccessDialog_title)",
"placeholders": {
"origin": {
"scope": {
"content": "$1"
},
"permission": {
Expand All @@ -413,13 +413,8 @@
}
},
"page_proxyAccessDialog_wildcardCheckbox_label": {
"message": "Apply to all permissions for $ORIGIN$",
"description": "Label for the apply permissions to all checkbox (page_proxyAccessDialog_wildcardCheckbox_label)",
"placeholders": {
"origin": {
"content": "$1"
}
}
"message": "Apply this decision to all permissions in this scope",
"description": "Label for the apply permissions to all checkbox (page_proxyAccessDialog_wildcardCheckbox_label)"
},
"page_proxyAcl_revoke_all_button_title": {
"message": "Revoke all permissions",
Expand Down
150 changes: 99 additions & 51 deletions add-on/src/lib/ipfs-proxy/access-control.js
Original file line number Diff line number Diff line change
Expand Up @@ -16,67 +16,108 @@ class AccessControl extends EventEmitter {

async _onStorageChange (changes) {
const prefix = this._storageKeyPrefix
const aclChangeKeys = Object.keys(changes).filter((key) => key.startsWith(prefix))
const scopesKey = this._getScopesKey()
const aclChangeKeys = Object.keys(changes).filter((key) => {
return key !== scopesKey && key.startsWith(prefix)
})

if (!aclChangeKeys.length) return

// Map { origin => Map { permission => allow } }
// Map { scope => Map { permission => allow } }
this.emit('change', aclChangeKeys.reduce((aclChanges, key) => {
return aclChanges.set(
key.slice(prefix.length + 1),
key.slice(prefix.length + ('.access'.length) + 1),
new Map(JSON.parse(changes[key].newValue))
)
}, new Map()))
}

_getGrantsKey (origin) {
return `${this._storageKeyPrefix}.${origin}`
_getScopesKey () {
return `${this._storageKeyPrefix}.scopes`
}

// Get a Map of granted permissions for a given origin
// e.g. Map { 'files.add' => true, 'object.new' => false }
async _getGrants (origin) {
const key = this._getGrantsKey(origin)
return new Map(
// Get the list of scopes stored in the acl
async _getScopes () {
const key = this._getScopesKey()
return new Set(
JSON.parse((await this._storage.local.get({ [key]: '[]' }))[key])
)
}

async _setGrants (origin, grants) {
const key = this._getGrantsKey(origin)
return this._storage.local.set({ [key]: JSON.stringify(Array.from(grants)) })
async _addScope (scope) {
const scopes = await this._getScopes()
scopes.add(scope)

const key = this._getScopesKey()
await this._storage.local.set({ [key]: JSON.stringify(Array.from(scopes)) })
}

// ordered by longest first
async _getMatchingScopes (scope) {
const scopes = await this._getScopes()
const origin = new URL(scope).origin

return Array.from(scopes)
.filter(s => {
if (origin !== new URL(s).origin) return false
return scope.startsWith(s)
})
.sort((a, b) => b.length - a.length)
}

async getAccess (origin, permission) {
if (!isOrigin(origin)) throw new TypeError('Invalid origin')
_getAccessKey (scope) {
return `${this._storageKeyPrefix}.access.${scope}`
}

// Get a Map of granted permissions for a given scope
// e.g. Map { 'files.add' => true, 'object.new' => false }
async _getAllAccess (scope) {
const key = this._getAccessKey(scope)
return new Map(
JSON.parse((await this._storage.local.get({ [key]: '[]' }))[key])
)
}

async getAccess (scope, permission) {
if (!isScope(scope)) throw new TypeError('Invalid scope')
if (!isString(permission)) throw new TypeError('Invalid permission')

const grants = await this._getGrants(origin)
const matchingScopes = await this._getMatchingScopes(scope)

let allow = null
let matchingScope

for (matchingScope of matchingScopes) {
const allAccess = await this._getAllAccess(matchingScope)

if (allAccess.has('*')) {
allow = allAccess.get('*')
break
}

if (grants.has('*')) {
return { origin, permission, allow: grants.get('*') }
if (allAccess.has(permission)) {
allow = allAccess.get(permission)
break
}
}

return grants.has(permission)
? { origin, permission, allow: grants.get(permission) }
: null
return allow == null ? null : { scope: matchingScope, permission, allow }
}

async setAccess (origin, permission, allow) {
if (!isOrigin(origin)) throw new TypeError('Invalid origin')
async setAccess (scope, permission, allow) {
if (!isScope(scope)) throw new TypeError('Invalid scope')
if (!isString(permission)) throw new TypeError('Invalid permission')
if (!isBoolean(allow)) throw new TypeError('Invalid allow')

return this._writeQ.add(async () => {
const access = { origin, permission, allow }
const grants = await this._getGrants(origin)
const allAccess = await this._getAllAccess(scope)

// Trying to set access for non-wildcard permission, when wildcard
// permission is already granted?
if (grants.has('*') && permission !== '*') {
if (grants.get('*') === allow) {
if (allAccess.has('*') && permission !== '*') {
if (allAccess.get('*') === allow) {
// Noop if requested access is the same as access for wildcard grant
return access
return { scope, permission, 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`)
Expand All @@ -85,47 +126,54 @@ class AccessControl extends EventEmitter {

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

grants.set(permission, allow)
await this._setGrants(origin, grants)
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 access
return { scope, permission, allow }
})
}

// Map { origin => Map { permission => allow } }
// Map { scope => Map { permission => allow } }
async getAcl () {
const data = await this._storage.local.get()
const prefix = this._storageKeyPrefix
const scopes = await this._getScopes()
const acl = new Map()

await Promise.all(Array.from(scopes).map(scope => {
return (async () => {
const allAccess = await this._getAllAccess(scope)
acl.set(scope, allAccess)
})()
}))

return Object.keys(data)
.reduce((acl, key) => {
return key.startsWith(prefix)
? acl.set(key.slice(prefix.length + 1), new Map(JSON.parse(data[key])))
: acl
}, new Map())
return acl
}

// Revoke access to the given permission
// if permission is null, revoke all access
async revokeAccess (origin, permission = null) {
if (!isOrigin(origin)) throw new TypeError('Invalid origin')
async revokeAccess (scope, permission = null) {
if (!isScope(scope)) throw new TypeError('Invalid scope')
if (permission && !isString(permission)) throw new TypeError('Invalid permission')

return this._writeQ.add(async () => {
let grants
let allAccess

if (permission) {
grants = await this._getGrants(origin)
if (!grants.has(permission)) return
grants.delete(permission)
allAccess = await this._getAllAccess(scope)
if (!allAccess.has(permission)) return
allAccess.delete(permission)
} else {
grants = new Map()
allAccess = new Map()
}

await this._setGrants(origin, grants)
const key = this._getAccessKey(scope)
await this._storage.local.set({ [key]: JSON.stringify(Array.from(allAccess)) })
})
}

Expand All @@ -136,7 +184,7 @@ class AccessControl extends EventEmitter {

module.exports = AccessControl

const isOrigin = (value) => {
const isScope = (value) => {
if (!isString(value)) return false

let url
Expand All @@ -147,7 +195,7 @@ const isOrigin = (value) => {
return false
}

return url.origin === value
return url.origin + url.pathname === value
}

const isString = (value) => Object.prototype.toString.call(value) === '[object String]'
Expand Down
8 changes: 6 additions & 2 deletions add-on/src/lib/ipfs-proxy/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -20,14 +20,18 @@ function createIpfsProxy (getIpfs, getState) {
const onPortConnect = (port) => {
if (port.name !== 'ipfs-proxy') return

const { origin } = new URL(port.sender.url)
const getScope = async () => {
const tab = await browser.tabs.get(port.sender.tab.id)
const { origin, pathname } = new URL(tab.url)
return origin + pathname
}

const proxy = createProxyServer(getIpfs, {
addListener: (_, handler) => port.onMessage.addListener(handler),
removeListener: (_, handler) => port.onMessage.removeListener(handler),
postMessage: (data) => port.postMessage(data),
getMessageData: (d) => d,
pre: (fnName) => createPreAcl(getState, accessControl, origin, fnName, requestAccess)
pre: (fnName) => createPreAcl(getState, accessControl, getScope, fnName, requestAccess)
})

const close = () => {
Expand Down
9 changes: 5 additions & 4 deletions add-on/src/lib/ipfs-proxy/pre-acl.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,19 +5,20 @@ 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 (getState, accessControl, origin, permission, requestAccess) {
function createPreAcl (getState, accessControl, getScope, permission, 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

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

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

if (!access.allow) throw new Error(`User denied access to ${permission}`)
Expand Down
Loading

0 comments on commit a5061d3

Please sign in to comment.