├── .gitignore ├── .travis.yml ├── LICENSE.md ├── README.md ├── package-lock.json ├── package.json ├── src └── acl-check.js └── test ├── resources ├── acl-container-ttl.js └── untyped-acl-ttl.js └── unit ├── access-denied-test.js ├── authorization-test.js ├── check-access-test.js ├── clear-permissions-test.js ├── configure-logger-test.js ├── get-permissions-test.js ├── get-trusted-modes-for-origin-test.js ├── module-index-test.js └── permission-set-test.js /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | lib 3 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | sudo: false 2 | 3 | language: node_js 4 | 5 | node_js: 6 | - "8" 7 | - "10" 8 | - "lts/*" 9 | - "node" 10 | 11 | cache: 12 | directories: 13 | - node_modules 14 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2016 - present 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # acl-check 2 | 3 | [![](https://img.shields.io/badge/project-Solid-7C4DFF.svg?style=flat)](https://github.com/solid/solid) 4 | [![NPM Version](https://img.shields.io/npm/v/acl-check.svg?style=flat)](https://npm.im/acl-check) 5 | [![Build Status](https://travis-ci.org/solid/acl-check.svg?branch=master)](https://travis-ci.org/solid/acl-check) 6 | 7 | Javascript library for checking [Web Access 8 | Control](https://github.com/solid/web-access-control-spec) ACLs. 9 | 10 | ## Usage 11 | 12 | ```js 13 | const $rdf = require('rdflib') 14 | const aclCheck = require('acl-check') 15 | const ACL = $rdf.Namespace('http://www.w3.org/ns/auth/acl#') 16 | 17 | const kb = $rdf.graph() 18 | const fetcher = $rdf.fetcher(kb) 19 | 20 | let doc = $rdf.sym('https://alice.example.com/stuff/myVacation.ttl') 21 | let aclDoc = $rdf.sym('https://alice.example.com/stuff/myVacation.ttl.acl') 22 | let directory = $rdf.sym('https://alice.example.com/stuff/') 23 | let dirAclDoc = $rdf.sym('https://alice.example.com/stuff/') 24 | 25 | let agent = $rdf.sym('https://alice.example.com/card.ttl#me') 26 | let modesRequired = [ ACL('Read'), ACL('Write'), ACL('Control') ] 27 | 28 | await fetcher.load(aclDoc) // Load the ACL documents into kb 29 | 30 | let allow = aclCheck.checkAccess(kb, resource, null, aclDoc, agent, modesRequired, origin, trustedOrigins) 31 | 32 | // When there is no direct ACL file, find the closest container ACL file in the tree above then... 33 | await fetcher.load(dirAclDoc) // Load the directory ACL documents into kb 34 | let allow = aclCheck.checkAccess(kb, resource, directory, dirAclDoc, agent, modesRequired, origin, trustedOrigins) 35 | 36 | console.log('Access allowed? ' + allow) 37 | // OWTTE 38 | ``` 39 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@solid/acl-check", 3 | "version": "0.4.5", 4 | "engines": { 5 | "node": ">=8.0" 6 | }, 7 | "description": "Web Access Control check access function", 8 | "main": "./lib/acl-check", 9 | "files": [ 10 | "lib" 11 | ], 12 | "scripts": { 13 | "build": "babel src -d lib", 14 | "preversion": "npm test", 15 | "postversion": "git push --follow-tags", 16 | "prepublish": "npm test && npm run build", 17 | "standard": "standard src/*.js", 18 | "tape": "tape test/unit/check-access-test.js test/unit/access-denied-test.js test/unit/configure-logger-test.js test/unit/get-trusted-modes-for-origin-test.js", 19 | "test": "npm run standard && npm run tape" 20 | }, 21 | "repository": { 22 | "type": "git", 23 | "url": "https://github.com/solid/acl-check" 24 | }, 25 | "keywords": [ 26 | "acl", 27 | "access", 28 | "control", 29 | "WAC", 30 | "solid", 31 | "decentralized", 32 | "web", 33 | "rdf", 34 | "ldp", 35 | "linked", 36 | "data" 37 | ], 38 | "author": "Tim Berners-Lee ", 39 | "contributor": [ 40 | "Kjetil Kjernsmo ", 41 | "Dmitri Zagidulin " 42 | ], 43 | "license": "MIT", 44 | "bugs": { 45 | "url": "https://github.com/solid/acl-check/issues" 46 | }, 47 | "homepage": "https://github.com/solid/acl-check", 48 | "dependencies": { 49 | "rdflib": "^2.1.7", 50 | "solid-namespace": "^0.5.0" 51 | }, 52 | "devDependencies": { 53 | "babel-cli": "^6.26.0", 54 | "babel-preset-es2015": "^6.24.1", 55 | "sinon": "^9.2.4", 56 | "solid-auth-cli": "^1.0.15", 57 | "solid-auth-client": "^2.5.5", 58 | "standard": "^16.0.3", 59 | "tape": "^5.1.1" 60 | }, 61 | "standard": { 62 | "globals": [] 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /src/acl-check.js: -------------------------------------------------------------------------------- 1 | // Access control logic 2 | 3 | const $rdf = require('rdflib') 4 | 5 | const ACL = $rdf.Namespace('http://www.w3.org/ns/auth/acl#') 6 | const FOAF = $rdf.Namespace('http://xmlns.com/foaf/0.1/') 7 | const VCARD = $rdf.Namespace('http://www.w3.org/2006/vcard/ns#') 8 | 9 | let _logger 10 | 11 | function publisherTrustedApp (kb, doc, aclDoc, modesRequired, origin, docAuths) { 12 | const app = $rdf.sym(origin) 13 | const appAuths = docAuths.filter(auth => kb.holds(auth, ACL('mode'), ACL('Control'), aclDoc)) 14 | const owners = appAuths.map(auth => kb.each(auth, ACL('agent'))).flat() // owners 15 | const relevant = owners.map(owner => kb.each(owner, ACL('trust'), null, owner.doc()).filter( 16 | ta => kb.holds(ta, ACL('trustedApp'), app, owner.doc()))).flat() // ta's 17 | const modesOK = relevant.map(ta => kb.each(ta, ACL('mode'))).flat().map(m => m.uri) 18 | const modesRequiredURIs = modesRequired.map(m => m.uri) 19 | modesRequiredURIs.every(uri => modesOK.includes(uri)) 20 | // modesRequired.every(mode => appAuths.some(auth => kb.holds(auth, ACL('mode'), mode, aclDoc))) 21 | } 22 | 23 | function accessDenied (kb, doc, directory, aclDoc, agent, modesRequired, origin, trustedOrigins, originTrustedModes = []) { 24 | log(`accessDenied: checking access to ${doc} by ${agent} and origin ${origin}`) 25 | const modeURIorReasons = modesAllowed(kb, doc, directory, aclDoc, agent, origin, trustedOrigins, originTrustedModes) 26 | let ok = false 27 | log('accessDenied: modeURIorReasons: ' + JSON.stringify(Array.from(modeURIorReasons))) 28 | modesRequired.forEach(mode => { 29 | log(' checking ' + mode) 30 | if (modeURIorReasons.has(mode.uri)) { 31 | log(' Mode required and allowed:' + mode) 32 | } else if (mode.sameTerm(ACL('Append')) && modeURIorReasons.has(ACL('Write').uri)) { 33 | log(' Append required and Write allowed. OK') 34 | } else { 35 | ok = modeURIorReasons.values().next().value || 'Forbidden' 36 | if (ok.startsWith('http')) { 37 | // Then, the situation is that one mode has failed, the other 38 | // has passed, and we get URI of the one that passed, but that's not a good error 39 | ok = 'All Required Access Modes Not Granted' 40 | } 41 | log(' MODE REQUIRED NOT ALLOWED: ' + mode + ' Denying with ' + ok) 42 | } 43 | }) 44 | return ok 45 | } 46 | 47 | async function getTrustedModesForOrigin (kb, doc, directory, aclDoc, origin, fetch) { 48 | // FIXME: this is duplicate code from the modesAllowed function, will refactor, 49 | // see https://github.com/solid/acl-check/issues/22 50 | let auths 51 | if (!directory) { // Normal case, ACL for a file 52 | auths = kb.each(null, ACL('accessTo'), doc, aclDoc) 53 | log(` ${auths.length} direct authentications about ${doc}`) 54 | } else { 55 | auths = kb.each(null, ACL('default'), directory, null) 56 | auths = auths.concat(kb.each(null, ACL('defaultForNew'), directory, null)) // Deprecated but keep for ages 57 | log(` ${auths.length} default authentications about ${directory} in ${aclDoc}`) 58 | } 59 | const ownerAuths = auths.filter(auth => kb.holds(auth, ACL('mode'), ACL('Control'), aclDoc)) 60 | const owners = ownerAuths.reduce((acc, auth) => acc.concat(kb.each(auth, ACL('agent'))), []) // owners 61 | let result 62 | try { 63 | result = await Promise.all(owners.map(owner => { 64 | return fetch(owner).then(() => { 65 | const q = ` 66 | SELECT ?mode WHERE { 67 | ${owner} ${ACL('trustedApp')} ?trustedOrigin. 68 | ?trustedOrigin ${ACL('origin')} ${origin}; 69 | ${ACL('mode')} ?mode . 70 | }` 71 | return query(q, kb) 72 | }).catch(e => { 73 | log('could not fetch owner doc', owner, e.message) 74 | }) 75 | })) 76 | } catch (e) { 77 | log('error checking owner profiles', e.message) 78 | } 79 | const trustedModes = [] 80 | try { 81 | result.forEach(ownerResults => ownerResults.forEach(entry => { 82 | trustedModes.push(entry['?mode']) 83 | })) 84 | } catch (e) { 85 | log('error processing owner results') 86 | } 87 | return Promise.resolve(trustedModes) 88 | } 89 | 90 | async function query (queryString, store) { 91 | return new Promise((resolve, reject) => { 92 | try { 93 | const query = $rdf.SPARQLToQuery(queryString, true, store) 94 | const results = [] 95 | store.query(query, (result) => { 96 | results.push(result) 97 | }, null, () => { 98 | resolve(results) 99 | }) 100 | } catch (err) { 101 | reject(err) 102 | } 103 | }) 104 | } 105 | 106 | /* Function checkAccess 107 | ** @param kb A quadstore 108 | ** @param doc the resource (A named node) or directory for which ACL applies 109 | */ 110 | function checkAccess (kb, doc, directory, aclDoc, agent, modesRequired, origin, trustedOrigins, originTrustedModes) { 111 | return !accessDenied(kb, doc, directory, aclDoc, agent, modesRequired, origin, trustedOrigins, originTrustedModes) 112 | } 113 | 114 | function modesAllowed (kb, doc, directory, aclDoc, agent, origin, trustedOrigins, originTrustedModes = []) { 115 | let auths 116 | if (!directory) { // Normal case, ACL for a file 117 | auths = kb.each(null, ACL('accessTo'), doc, aclDoc) 118 | log(` ${auths.length} direct authentications about ${doc}`) 119 | } else { 120 | auths = kb.each(null, ACL('default'), directory, null) 121 | auths = auths.concat(kb.each(null, ACL('defaultForNew'), directory, null)) // Deprecated but keep for ages 122 | log(` ${auths.length} default authentications about ${directory} in ${aclDoc}`) 123 | } 124 | if (origin && trustedOrigins && nodesIncludeNode(trustedOrigins, origin)) { 125 | log('Origin ' + origin + ' is trusted') 126 | origin = null // stop worrying about origin 127 | log(` modesAllowed: Origin ${origin} is trusted.`) 128 | } 129 | 130 | function agentOrGroupOK (auth, agent) { 131 | log(` Checking auth ${auth} with agent ${agent}`) 132 | if (!agent) { 133 | log(' Agent or group: Fail: not public and not logged on.') 134 | return false 135 | } 136 | if (kb.holds(auth, ACL('agentClass'), ACL('AuthenticatedAgent'), aclDoc)) { 137 | log(' AuthenticatedAgent: logged in, looks good') 138 | return true 139 | } 140 | if (kb.holds(auth, ACL('agent'), agent, aclDoc)) { 141 | log(' Agent explicitly authenticated.') 142 | return true 143 | } 144 | if (kb.each(auth, ACL('agentGroup'), null, aclDoc).some( 145 | group => kb.holds(group, VCARD('hasMember'), agent, group.doc()))) { 146 | log(' Agent is member of group which has access.') 147 | return true 148 | } 149 | log(' Agent or group access fails for this authentication.') 150 | return false 151 | } // Agent or group 152 | 153 | function originOK (auth, origin) { 154 | return kb.holds(auth, ACL('origin'), origin, aclDoc) 155 | } 156 | 157 | function agentAndAppFail (auth) { 158 | if (kb.holds(auth, ACL('agentClass'), FOAF('Agent'), aclDoc)) { 159 | log(' Agent or group: Ok, its public.') 160 | return false 161 | } 162 | if (!agentOrGroupOK(auth, agent)) { 163 | log(' The agent/group check fails') 164 | return 'User Unauthorized' 165 | } 166 | if (!origin) { 167 | log(' Origin check not needed: no origin.') 168 | return false 169 | } 170 | if (originTrustedModes && originTrustedModes.length > 0) { 171 | log(` Origin might have access (${originTrustedModes.join(', ')})`) 172 | return false 173 | } 174 | if (originOK(auth, origin)) { 175 | log(' Origin check succeeded.') 176 | return false 177 | } 178 | log(' Origin check FAILED. Origin not trusted.') 179 | return 'Origin Unauthorized' // @@ look for other trusted apps 180 | } 181 | 182 | const modeURIorReasons = new Set() 183 | 184 | auths.forEach(auth => { 185 | const agentAndAppStatus = agentAndAppFail(auth) 186 | if (agentAndAppStatus) { 187 | log(' Check failed: ' + agentAndAppStatus) 188 | modeURIorReasons.add(agentAndAppStatus) 189 | } else { 190 | let modes = kb.each(auth, ACL('mode'), null, aclDoc) 191 | // If there IS an origin, check that those modes are allowed to it too 192 | if (origin && originTrustedModes && originTrustedModes.length > 0) { 193 | modes = modes.filter(mode => nodesIncludeNode(originTrustedModes, mode)) 194 | } 195 | modes.forEach(mode => { 196 | log(' Mode allowed: ' + mode) 197 | modeURIorReasons.add(mode.uri) 198 | }) 199 | } 200 | }) 201 | return modeURIorReasons 202 | } 203 | 204 | function nodesIncludeNode (nodes, node) { 205 | return nodes.some(trustedOrigin => trustedOrigin.termType === node.termType && trustedOrigin.value === node.value) 206 | } 207 | 208 | function configureLogger (logger) { 209 | _logger = logger 210 | } 211 | 212 | function log (...msgs) { 213 | return (_logger || console.log).apply(_logger, msgs) 214 | } 215 | 216 | module.exports.accessDenied = accessDenied 217 | module.exports.checkAccess = checkAccess 218 | module.exports.configureLogger = configureLogger 219 | module.exports.getTrustedModesForOrigin = getTrustedModesForOrigin 220 | module.exports.log = log 221 | module.exports.modesAllowed = modesAllowed 222 | module.exports.publisherTrustedApp = publisherTrustedApp 223 | -------------------------------------------------------------------------------- /test/resources/acl-container-ttl.js: -------------------------------------------------------------------------------- 1 | module.exports = `@prefix acl: . 2 | @prefix foaf: . 3 | 4 | <#authorization1> 5 | a acl:Authorization; 6 | 7 | # These statements specify access rules for the /docs/ container itself: 8 | acl:agent 9 | , 10 | , 11 | , 12 | ; 13 | acl:accessTo ; 14 | acl:mode 15 | acl:Read, acl:Write, acl:Control; 16 | 17 | acl:origin 18 | ; 19 | 20 | # defaultForNew says: this authorization (the statements above) will also 21 | # be inherited by any resource within that container that doesn't have its 22 | # own ACL. 23 | acl:defaultForNew . 24 | # soon to be: 25 | # acl:default . 26 | 27 | <#authorization2> 28 | a acl:Authorization; 29 | acl:agentClass foaf:Agent; # everyone 30 | acl:mode acl:Read; # has Read-only access 31 | acl:accessTo . # to the public profile` 32 | -------------------------------------------------------------------------------- /test/resources/untyped-acl-ttl.js: -------------------------------------------------------------------------------- 1 | module.exports = `@prefix acl: . 2 | @prefix foaf: . 3 | 4 | <#authorization> 5 | # Not present: a acl:Authorization; 6 | 7 | acl:agent 8 | ; 9 | acl:accessTo ; 10 | acl:mode 11 | acl:Read, acl:Write, acl:Control.` 12 | -------------------------------------------------------------------------------- /test/unit/access-denied-test.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const test = require('tape') 4 | const aclLogic = require('../../src/acl-check') 5 | const $rdf = require('rdflib') 6 | 7 | const ACL = $rdf.Namespace('http://www.w3.org/ns/auth/acl#') 8 | const FOAF = $rdf.Namespace('http://xmlns.com/foaf/0.1/') 9 | const ALICE = $rdf.Namespace('https://alice.example.com/') 10 | 11 | const prefixes = `@prefix acl: . 12 | @prefix foaf: . 13 | @prefix alice: . 14 | @prefix bob: . 15 | ` 16 | const alice = $rdf.sym('https://alice.example.com/#me') 17 | const bob = $rdf.sym('https://bob.example.com/#me') 18 | const malory = $rdf.sym('https://someone.else.example.com/') 19 | 20 | // Append access implied by Write acecss 21 | test('aclCheck accessDenied() test - Append access implied by Write acecss', t => { 22 | const resource = $rdf.sym('https://alice.example.com/docs/file1') 23 | const aclUrl = 'https://alice.example.com/docs/.acl' 24 | const aclDoc = $rdf.sym(aclUrl) 25 | 26 | const store = $rdf.graph() // Quad store 27 | const ACLtext = prefixes + 28 | ` <#auth> a acl:Authorization; 29 | acl:mode acl:Write; 30 | acl:agent alice:me; 31 | acl:accessTo <${resource.uri}> . 32 | ` 33 | $rdf.parse(ACLtext, store, aclUrl, 'text/turtle') 34 | 35 | const agent = alice 36 | const directory = null 37 | const modesRequired = [ACL('Append')] 38 | const trustedOrigins = null 39 | const origin = null 40 | 41 | const result = !aclLogic.accessDenied(store, resource, directory, aclDoc, agent, modesRequired, origin, trustedOrigins) 42 | t.ok(result, 'Alice should have Append access implied by Write access') 43 | t.end() 44 | }) 45 | 46 | // Straight ACL access test 47 | test('acl-check accessDenied() test - accessTo', function (t) { 48 | const container = $rdf.sym('https://alice.example.com/docs/') 49 | const containerAclUrl = 'https://alice.example.com/docs/.acl' 50 | const containerAcl = $rdf.sym(containerAclUrl) 51 | 52 | const store = $rdf.graph() // Quad store 53 | const ACLtext = prefixes + 54 | ` <#auth> a acl:Authorization; 55 | acl:mode acl:Read, acl:Write; 56 | acl:agent alice:me; 57 | acl:accessTo <${container.uri}> . 58 | ` 59 | $rdf.parse(ACLtext, store, containerAclUrl, 'text/turtle') 60 | 61 | var result = aclLogic.accessDenied(store, container, null, containerAcl, bob, [ACL('Write')]) 62 | t.ok(result, 'Bob Should not have access') 63 | t.equal(result, 'User Unauthorized', 'Correct reason') 64 | 65 | t.end() 66 | }) 67 | 68 | // Inheriting permissions from directory defaults 69 | test('acl-check accessDenied() test - default/inherited', function (t) { 70 | const container = $rdf.sym('https://alice.example.com/docs/') 71 | const containerAcl = $rdf.sym('https://alice.example.com/docs/.acl') 72 | const file1 = $rdf.sym('https://alice.example.com/docs/file1') 73 | const file2 = $rdf.sym('https://alice.example.com/docs/stuff/file2') 74 | var result 75 | const store = $rdf.graph() 76 | /* 77 | let ACLtext = prefixes + ` <#auth> a acl:Authorization; 78 | acl:mode acl:Read; 79 | acl:agent bob:me; 80 | acl:accessTo <${file1.uri}> . 81 | ` 82 | $rdf.parse(ACLtext, store, containerAcl.uri, 'text/turtle') 83 | */ 84 | const containerAclText = prefixes + ` <#auth> a acl:Authorization; 85 | acl:mode acl:Read; 86 | acl:agent alice:me; 87 | acl:default <${container.uri}> . 88 | ` 89 | $rdf.parse(containerAclText, store, containerAcl.uri, 'text/turtle') 90 | 91 | result = !aclLogic.accessDenied(store, file1, container, containerAcl, alice, [ACL('Read')]) 92 | t.ok(result, 'Alice should have Read access inherited') 93 | 94 | result = !aclLogic.accessDenied(store, file2, container, containerAcl, alice, [ACL('Read')]) 95 | t.ok(result, 'Alice should have Read access inherited 2') 96 | 97 | result = aclLogic.accessDenied(store, file2, container, containerAcl, alice, [ACL('Write')]) 98 | t.ok(result, 'Alice should NOT have Write access inherited') 99 | 100 | t.end() 101 | }) 102 | 103 | // Inheriting permissions from directory defaults 104 | test('acl-check accessDenied() test - default/inherited', function (t) { 105 | const container = $rdf.sym('https://alice.example.com/docs/') 106 | const containerAcl = $rdf.sym('https://alice.example.com/docs/.acl') 107 | const file1 = $rdf.sym('https://alice.example.com/docs/file1') 108 | const file2 = $rdf.sym('https://alice.example.com/docs/stuff/file2') 109 | const origin = $rdf.sym('https://apps.example.com') 110 | const malorigin = $rdf.sym('https://mallory.example.com') 111 | const trustedOrigins = null 112 | var result 113 | const store = $rdf.graph() 114 | /* 115 | let ACLtext = prefixes + ` <#auth> a acl:Authorization; 116 | acl:mode acl:Read; 117 | acl:agent bob:me; 118 | acl:accessTo <${file1.uri}> . 119 | ` 120 | $rdf.parse(ACLtext, store, containerAcl.uri, 'text/turtle') 121 | */ 122 | const containerAclText = prefixes + ` <#auth> a acl:Authorization; 123 | acl:mode acl:Read; 124 | acl:agentClass foaf:Agent; 125 | acl:default <${container.uri}> . 126 | ` 127 | $rdf.parse(containerAclText, store, containerAcl.uri, 'text/turtle') 128 | console.log('@@' + containerAclText + '@@@') 129 | 130 | result = !aclLogic.accessDenied(store, file2, container, containerAcl, alice, [ACL('Read')]) 131 | t.ok(result, 'Alice should have read access - Public') 132 | 133 | result = !aclLogic.accessDenied(store, file2, container, containerAcl, bob, [ACL('Read')]) 134 | t.ok(result, 'Bob should have read access too - Public') 135 | 136 | result = !aclLogic.accessDenied(store, file2, container, containerAcl, alice, [ACL('Read')], origin, trustedOrigins) 137 | t.ok(result, 'Alice should have read access regardless of origin - Public') 138 | 139 | result = !aclLogic.accessDenied(store, file2, container, containerAcl, bob, [ACL('Read')], origin, trustedOrigins) 140 | t.ok(result, 'Bob should have read access too regardless of origin - Public') 141 | 142 | result = !aclLogic.accessDenied(store, file2, container, containerAcl, alice, [ACL('Read')], malorigin, trustedOrigins) 143 | t.ok(result, 'Alice should have read access even with wrong origin - Public') 144 | 145 | result = !aclLogic.accessDenied(store, file2, container, containerAcl, bob, [ACL('Read')], malorigin, trustedOrigins) 146 | t.ok(result, 'Bob should have read access too even with wrong origin - Public') 147 | 148 | result = aclLogic.accessDenied(store, file2, container, containerAcl, alice, [ACL('Write')]) 149 | t.ok(result, 'Alice should NOT have write access inherited - Public') 150 | 151 | t.end() 152 | }) 153 | 154 | // Straight ACL access test 155 | test('acl-check accessDenied() test - accessTo', function (t) { 156 | const container = $rdf.sym('https://alice.example.com/docs/') 157 | const containerAclUrl = 'https://alice.example.com/docs/.acl' 158 | const containerAcl = $rdf.sym(containerAclUrl) 159 | 160 | const store = $rdf.graph() // Quad store 161 | const ACLtext = prefixes + 162 | ` <#auth> a acl:Authorization; 163 | acl:mode acl:Read, acl:Write; 164 | acl:agentClass acl:AuthenticatedAgent; 165 | acl:accessTo <${container.uri}> . 166 | ` 167 | $rdf.parse(ACLtext, store, containerAclUrl, 'text/turtle') 168 | 169 | var result = aclLogic.accessDenied(store, container, null, containerAcl, null, [ACL('Read')]) 170 | t.ok(result, 'Anonymous should NOT have Read access to public thing - AuthenticatedAgent') 171 | 172 | result = aclLogic.accessDenied(store, container, null, containerAcl, null, [ACL('Write')]) 173 | t.ok(result, 'Anonymous should NOT have Write access - AuthenticatedAgent') 174 | 175 | result = !aclLogic.accessDenied(store, container, null, containerAcl, bob, [ACL('Write')]) 176 | t.ok(result, 'Bob should have Write access to public write - AuthenticatedAgent') 177 | 178 | t.end() 179 | }) 180 | 181 | // Inheriting permissions from directory defaults 182 | test('acl-check accessDenied() test - default/inherited', function (t) { 183 | const container = $rdf.sym('https://alice.example.com/docs/') 184 | const containerAcl = $rdf.sym('https://alice.example.com/docs/.acl') 185 | const file1 = $rdf.sym('https://alice.example.com/docs/file1') 186 | const file2 = $rdf.sym('https://alice.example.com/docs/stuff/file2') 187 | var result 188 | const store = $rdf.graph() 189 | const ACLtext = prefixes + ` <#auth> a acl:Authorization; 190 | acl:mode acl:Read; 191 | acl:agent bob:me; 192 | acl:accessTo <${file1.uri}> . 193 | ` 194 | $rdf.parse(ACLtext, store, containerAcl.uri, 'text/turtle') 195 | 196 | const containerAclText = prefixes + ` <#auth> a acl:Authorization; 197 | acl:mode acl:Read; 198 | acl:agentClass acl:AuthenticatedAgent; 199 | acl:default <${container.uri}> . 200 | ` 201 | $rdf.parse(containerAclText, store, containerAcl.uri, 'text/turtle') 202 | 203 | result = aclLogic.accessDenied(store, file2, container, containerAcl, alice, [ACL('Write')]) 204 | t.ok(result, 'Alice should NOT have write access inherited - AuthenticatedAgent') 205 | 206 | t.end() 207 | }) 208 | 209 | // Append access implied by Write acecss 210 | test('aclCheck accessDenied() test - Append access implied by Write acecss', t => { 211 | const resource = $rdf.sym('https://alice.example.com/docs/file1') 212 | const aclUrl = 'https://alice.example.com/docs/.acl' 213 | const aclDoc = $rdf.sym(aclUrl) 214 | 215 | const origin = $rdf.sym('https://apps.example.com') 216 | const malorigin = $rdf.sym('https://mallory.example.com') 217 | const store = $rdf.graph() // Quad store 218 | const ACLtext = prefixes + 219 | ` <#auth> a acl:Authorization; 220 | acl:mode acl:Write; 221 | acl:agent alice:me; 222 | acl:origin <${origin.uri}> ; 223 | acl:accessTo <${resource.uri}> . 224 | ` 225 | $rdf.parse(ACLtext, store, aclUrl, 'text/turtle') 226 | 227 | const agent = alice 228 | const directory = null 229 | const modesRequired = [ACL('Append')] 230 | const trustedOrigins = null 231 | 232 | var result = !aclLogic.accessDenied(store, resource, directory, aclDoc, agent, modesRequired, origin, trustedOrigins) 233 | t.ok(result, 'App should have Append access implied by Write access with authorized origin') 234 | 235 | result = aclLogic.accessDenied(store, resource, directory, aclDoc, agent, modesRequired, malorigin, trustedOrigins) 236 | t.ok(result, 'Mallorys app should not have Append access with false origin') 237 | t.equal(result, 'Origin Unauthorized', 'Correct reason') 238 | 239 | t.end() 240 | }) 241 | 242 | test('aclCheck accessDenied() test - Read, Write and Append', t => { 243 | const resource = $rdf.sym('https://alice.example.com/docs/file1') 244 | const aclUrl = 'https://alice.example.com/docs/.acl' 245 | const aclDoc = $rdf.sym(aclUrl) 246 | 247 | const origin = $rdf.sym('https://apps.example.com') 248 | const malorigin = $rdf.sym('https://mallory.example.com') 249 | const store = $rdf.graph() // Quad store 250 | const ACLtext = prefixes + 251 | ` <#auth> a acl:Authorization; 252 | acl:mode acl:Write, acl:Read; 253 | acl:agent alice:me; 254 | acl:origin <${origin.uri}> ; 255 | acl:accessTo <${resource.uri}> . 256 | ` 257 | $rdf.parse(ACLtext, store, aclUrl, 'text/turtle') 258 | 259 | const agent = alice 260 | const directory = null 261 | const modesRequired = [ACL('Append'), ACL('Read'), ACL('Write')] 262 | const trustedOrigins = null 263 | 264 | var result = !aclLogic.accessDenied(store, resource, directory, aclDoc, agent, modesRequired, origin, trustedOrigins) 265 | t.ok(result, 'App should have access with authorized origin') 266 | 267 | result = aclLogic.accessDenied(store, resource, directory, aclDoc, agent, modesRequired, malorigin, trustedOrigins) 268 | t.ok(result, 'Mallorys app should not have access with false origin') 269 | t.equal(result, 'Origin Unauthorized', 'Correct reason') 270 | 271 | result = aclLogic.accessDenied(store, resource, directory, aclDoc, bob, modesRequired, origin, trustedOrigins) 272 | t.ok(result, 'Bob should not have access with correct origin') 273 | t.equal(result, 'User Unauthorized', 'Correct reason') 274 | 275 | result = aclLogic.accessDenied(store, resource, directory, aclDoc, bob, modesRequired, malorigin, trustedOrigins) 276 | t.ok(result, 'Bob should not have access with false origin') 277 | t.equal(result, 'User Unauthorized', 'Correct reason') 278 | 279 | result = aclLogic.accessDenied(store, resource, directory, aclDoc, agent, modesRequired, malorigin, trustedOrigins) 280 | t.ok(result, 'Mallorys app should not have access with false origin') 281 | t.equal(result, 'Origin Unauthorized', 'Correct reason') 282 | 283 | t.end() 284 | }) 285 | 286 | test('aclCheck accessDenied() test - Various access rules', t => { 287 | const resource = $rdf.sym('https://alice.example.com/docs/file1') 288 | const aclUrl = 'https://alice.example.com/docs/.acl' 289 | const aclDoc = $rdf.sym(aclUrl) 290 | 291 | const origin = $rdf.sym('https://apps.example.com') 292 | const malorigin = $rdf.sym('https://mallory.example.com') 293 | const store = $rdf.graph() // Quad store 294 | const ACLtext = prefixes + 295 | ` <#auth> a acl:Authorization; 296 | acl:mode acl:Read; 297 | acl:agent alice:me; 298 | acl:origin <${origin.uri}> ; 299 | acl:accessTo <${resource.uri}> . 300 | ` 301 | $rdf.parse(ACLtext, store, aclUrl, 'text/turtle') 302 | 303 | const agent = alice 304 | const directory = null 305 | var modesRequired = [ACL('Read')] 306 | const trustedOrigins = null 307 | 308 | var result = !aclLogic.accessDenied(store, resource, directory, aclDoc, agent, modesRequired, origin, trustedOrigins) 309 | t.ok(result, 'App should have Write access with authorized origin, only fulfilled modes') 310 | 311 | result = aclLogic.accessDenied(store, resource, directory, aclDoc, agent, modesRequired, malorigin, trustedOrigins) 312 | t.ok(result, 'Mallorys app should not have Write access with false origin, only fulfilled modes') 313 | t.equal(result, 'Origin Unauthorized', 'Correct reason') 314 | 315 | result = aclLogic.accessDenied(store, resource, directory, aclDoc, bob, modesRequired, origin, trustedOrigins) 316 | t.ok(result, 'Bob should not have Write access with correct origin, only fulfilled modes') 317 | t.equal(result, 'User Unauthorized', 'Correct reason') 318 | 319 | result = aclLogic.accessDenied(store, resource, directory, aclDoc, bob, modesRequired, malorigin, trustedOrigins) 320 | t.ok(result, 'Bob should not have Write access with false origin, only fulfilled modes') 321 | t.equal(result, 'User Unauthorized', 'Correct reason') 322 | 323 | result = aclLogic.accessDenied(store, resource, directory, aclDoc, agent, modesRequired, malorigin, trustedOrigins) 324 | t.ok(result, 'Mallorys app should not have Write access with false origin, only fulfilled modes') 325 | t.equal(result, 'Origin Unauthorized', 'Correct reason') 326 | 327 | modesRequired = [ACL('Write')] 328 | 329 | result = aclLogic.accessDenied(store, resource, directory, aclDoc, agent, modesRequired, malorigin, trustedOrigins) 330 | t.ok(result, 'Mallorys app should not have Write access with false origin, invalid modes') 331 | t.equal(result, 'Origin Unauthorized', 'Correct reason') 332 | 333 | result = aclLogic.accessDenied(store, resource, directory, aclDoc, bob, modesRequired, origin, trustedOrigins) 334 | t.ok(result, 'Bob should not have Write access with correct origin, invalid modes') 335 | t.equal(result, 'User Unauthorized', 'Correct reason') 336 | 337 | result = aclLogic.accessDenied(store, resource, directory, aclDoc, bob, modesRequired, malorigin, trustedOrigins) 338 | t.ok(result, 'Bob should not have Write access with false origin, invalid modes') 339 | t.equal(result, 'User Unauthorized', 'Correct reason') 340 | 341 | result = aclLogic.accessDenied(store, resource, directory, aclDoc, agent, modesRequired, malorigin, trustedOrigins) 342 | t.ok(result, 'Mallorys app should not have Write access with false origin, invalid modes') 343 | t.equal(result, 'Origin Unauthorized', 'Correct reason') 344 | 345 | modesRequired = [ACL('Write'), ACL('Read')] 346 | 347 | result = aclLogic.accessDenied(store, resource, directory, aclDoc, agent, modesRequired, origin, trustedOrigins) 348 | t.ok(result, 'Alice should not have Read and Write access with authorized origin, both modes') 349 | t.equal(result, 'All Required Access Modes Not Granted', 'Correct reason') 350 | 351 | result = aclLogic.accessDenied(store, resource, directory, aclDoc, agent, modesRequired, malorigin, trustedOrigins) 352 | t.ok(result, 'Mallorys app should not have Read and Write access with false origin, both modes') 353 | t.equal(result, 'Origin Unauthorized', 'Correct reason') 354 | 355 | result = aclLogic.accessDenied(store, resource, directory, aclDoc, bob, modesRequired, origin, trustedOrigins) 356 | t.ok(result, 'Bob should not have Read and Write access with correct origin, both modes') 357 | t.equal(result, 'User Unauthorized', 'Correct reason') 358 | 359 | result = aclLogic.accessDenied(store, resource, directory, aclDoc, bob, modesRequired, malorigin, trustedOrigins) 360 | t.ok(result, 'Bob should not have Read and Write access with false origin, both modes') 361 | t.equal(result, 'User Unauthorized', 'Correct reason') 362 | 363 | result = aclLogic.accessDenied(store, resource, directory, aclDoc, agent, modesRequired, malorigin, trustedOrigins) 364 | t.ok(result, 'Mallorys app should not have Write access with false origin, both modes') 365 | t.equal(result, 'Origin Unauthorized', 'Correct reason') 366 | 367 | t.end() 368 | }) 369 | 370 | test('aclCheck accessDenied() test - With trustedOrigins', t => { 371 | const resource = $rdf.sym('https://alice.example.com/docs/file1') 372 | const aclUrl = 'https://alice.example.com/docs/.acl' 373 | const aclDoc = $rdf.sym(aclUrl) 374 | 375 | const origin = $rdf.sym('https://apps.example.com') 376 | const malorigin = $rdf.sym('https://mallory.example.com') 377 | const store = $rdf.graph() // Quad store 378 | const ACLtext = prefixes + 379 | ` <#auth> a acl:Authorization; 380 | acl:mode acl:Read; 381 | acl:agent alice:me; 382 | acl:origin <${origin.uri}> ; 383 | acl:accessTo <${resource.uri}> . 384 | ` 385 | $rdf.parse(ACLtext, store, aclUrl, 'text/turtle') 386 | 387 | const agent = alice 388 | const directory = null 389 | var modesRequired = [ACL('Read')] 390 | const trustedOrigins = [$rdf.sym('https://apps.example.com')] 391 | 392 | var result = !aclLogic.accessDenied(store, resource, directory, aclDoc, agent, modesRequired, origin, trustedOrigins) 393 | t.ok(result, 'Should get access when origin is trusted') 394 | 395 | var result = aclLogic.accessDenied(store, resource, directory, aclDoc, agent, modesRequired, malorigin, trustedOrigins) 396 | t.ok(result, 'Should not get access when origin is not trusted') 397 | 398 | t.end() 399 | }) 400 | 401 | test('aclCheck accessDenied() test - with use of originTrustedModes', t => { 402 | const resource = ALICE('docs/file1') 403 | const aclDoc = ALICE('docs/.acl') 404 | const aclUrl = aclDoc.uri 405 | 406 | const origin = $rdf.sym('https://apps.example.com') 407 | const aclStore = $rdf.graph() 408 | // grants read, write and control access to Alice 409 | const ACLtext = `${prefixes} 410 | <#auth> a acl:Authorization; 411 | acl:mode acl:Read, acl:Write, acl:Control; 412 | acl:agent alice:me; 413 | acl:accessTo ${resource} . 414 | ` 415 | $rdf.parse(ACLtext, aclStore, aclUrl, 'text/turtle') 416 | 417 | const agent = alice 418 | const directory = null 419 | const trustedOrigins = [] 420 | const originTrustedModes = [ACL('Read'), ACL('Write')] 421 | 422 | const readWriteModeRequired = [ACL('Read'), ACL('Write')] 423 | const readWriteModeResult = aclLogic.accessDenied(aclStore, resource, directory, aclDoc, agent, readWriteModeRequired, origin, trustedOrigins, originTrustedModes) 424 | t.ok(!readWriteModeResult, 'Should get access to modes when origin is listed as trusted app') 425 | 426 | const controlModeRequired = [ACL('Control')] 427 | const controlModeResult = aclLogic.accessDenied(aclStore, resource, directory, aclDoc, agent, controlModeRequired, origin, trustedOrigins, originTrustedModes) 428 | t.ok(controlModeResult, 'All Required Access Modes Not Granted', 'Correct reason') 429 | 430 | // See https://github.com/solid/acl-check/issues/36 431 | const controlModeRequired2 = [ACL('Control')] 432 | const controlModeResult2 = aclLogic.accessDenied(aclStore, resource, directory, aclDoc, agent, controlModeRequired2, null, trustedOrigins, originTrustedModes) 433 | t.ok(!controlModeResult2, 'Should get access irrespective of origin modes modes when No origin', 'Correct reason 2') 434 | 435 | t.end() 436 | }) 437 | -------------------------------------------------------------------------------- /test/unit/authorization-test.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | var test = require('tape') 4 | // var rdf = require('rdflib') 5 | var Authorization = require('../../src/authorization') 6 | const { acl } = require('../../src/modes') 7 | 8 | const resourceUrl = 'https://bob.example.com/docs/file1' 9 | const agentWebId = 'https://bob.example.com/profile/card#me' 10 | // Not really sure what group webIDs will look like, not yet implemented: 11 | const groupWebId = 'https://devteam.example.com/something' 12 | 13 | test('a new Authorization()', function (t) { 14 | const auth = new Authorization() 15 | t.notOk(auth.isAgent()) 16 | t.notOk(auth.isGroup()) 17 | t.notOk(auth.isPublic()) 18 | t.notOk(auth.webId()) 19 | t.notOk(auth.resourceUrl) 20 | t.equal(auth.accessType, acl.ACCESS_TO) 21 | t.deepEqual(auth.mailTo, []) 22 | t.deepEqual(auth.allOrigins(), []) 23 | t.deepEqual(auth.allModes(), []) 24 | t.notOk(auth.isInherited(), 25 | 'An Authorization should not be inherited (acl:default) by default') 26 | t.ok(auth.isEmpty(), 'a new Authorization should be empty') 27 | t.end() 28 | }) 29 | 30 | test('a new Authorization for a container', function (t) { 31 | const auth = new Authorization(resourceUrl, acl.INHERIT) 32 | t.equal(auth.resourceUrl, resourceUrl) 33 | t.notOk(auth.webId()) 34 | t.notOk(auth.allowsRead()) 35 | t.notOk(auth.allowsWrite()) 36 | t.notOk(auth.allowsAppend()) 37 | t.notOk(auth.allowsControl()) 38 | t.ok(auth.isInherited(), 39 | 'Authorizations for containers should be inherited by default') 40 | t.equal(auth.accessType, acl.DEFAULT) 41 | t.end() 42 | }) 43 | 44 | test('Authorization allowsMode() test', function (t) { 45 | const auth = new Authorization() 46 | auth.addMode(acl.WRITE) 47 | t.ok(auth.allowsMode(acl.WRITE), 'auth.allowsMode() should work') 48 | t.end() 49 | }) 50 | 51 | test('an Authorization allows editing permission modes', function (t) { 52 | const auth = new Authorization() 53 | auth.addMode(acl.CONTROL) 54 | t.notOk(auth.isEmpty(), 'Adding an access mode means no longer empty') 55 | t.ok(auth.allowsControl(), 'Adding Control mode failed') 56 | t.notOk(auth.allowsRead(), 'Control mode should not imply Read') 57 | t.notOk(auth.allowsWrite(), 'Control mode should not imply Write') 58 | t.notOk(auth.allowsAppend(), 'Control mode should not imply Append') 59 | // Notice addMode() is chainable: 60 | auth 61 | .addMode(acl.READ) 62 | .addMode(acl.WRITE) 63 | t.ok(auth.allowsRead(), 'Adding Read mode failed') 64 | t.ok(auth.allowsWrite(), 'Adding Write mode failed') 65 | t.equals(auth.allModes().length, 3) 66 | auth.removeMode(acl.READ) 67 | t.notOk(auth.allowsRead(), 'Removing Read mode failed') 68 | auth.removeMode(acl.CONTROL) 69 | t.notOk(auth.allowsControl(), 'Removing Control mode failed') 70 | 71 | // Note that removing Append mode while retaining Write mode has no effect 72 | auth.removeMode(acl.APPEND) 73 | t.ok(auth.allowsWrite(), 'Removing Append should not remove Write mode') 74 | t.ok(auth.allowsAppend(), 75 | 'Removing Append while retaining Write mode should have no effect') 76 | 77 | auth.removeMode(acl.WRITE) 78 | t.notOk(auth.allowsWrite(), 'Removing Write mode failed') 79 | t.end() 80 | }) 81 | 82 | test('an Authorization can add or remove multiple modes', function (t) { 83 | const auth = new Authorization() 84 | auth.addMode([acl.READ, acl.WRITE, acl.CONTROL]) 85 | t.ok(auth.allowsRead() && auth.allowsWrite() && auth.allowsControl()) 86 | auth.removeMode([acl.WRITE, acl.READ]) 87 | t.notOk(auth.allowsRead() && auth.allowsWrite()) 88 | t.ok(auth.allowsControl()) 89 | t.end() 90 | }) 91 | 92 | test('an Authorization can only have either an agent or a group', function (t) { 93 | const auth1 = new Authorization() 94 | auth1.setAgent(agentWebId) 95 | t.equal(auth1.agent, agentWebId) 96 | // Try to set a group while an agent already set 97 | t.throws(function () { 98 | auth1.setGroup(groupWebId) 99 | }, 'Trying to set a group for an auth with an agent should throw an error') 100 | // Now try the other way -- setting an agent while a group is set 101 | const auth2 = new Authorization() 102 | auth2.setGroup(groupWebId) 103 | t.equal(auth2.group, groupWebId) 104 | t.throws(function () { 105 | auth2.setAgent(agentWebId) 106 | }, 'Trying to set an agent for an auth with a group should throw an error') 107 | t.end() 108 | }) 109 | 110 | test('acl.WRITE implies acl.APPEND', function (t) { 111 | let auth = new Authorization() 112 | auth.addMode(acl.WRITE) 113 | t.ok(auth.allowsWrite()) 114 | t.ok(auth.allowsAppend(), 'Adding Write mode implies granting Append mode') 115 | // But not the other way around 116 | auth = new Authorization() 117 | auth.addMode(acl.APPEND) 118 | t.ok(auth.allowsAppend(), 'Adding Append mode failed') 119 | t.notOk(auth.allowsWrite(), 'Adding Append mode should not grant Write mode') 120 | 121 | auth.removeMode(acl.WRITE) 122 | t.ok(auth.allowsAppend(), 123 | 'Removing Write mode when the auth only had Append mode should do nothing') 124 | 125 | auth.removeMode(acl.APPEND) 126 | t.notOk(auth.allowsAppend(), 'Removing Append mode failed') 127 | t.end() 128 | }) 129 | 130 | test('an Authorization can grant Public access', function (t) { 131 | let auth = new Authorization() 132 | t.notOk(auth.isPublic(), 'An authorization is not public access by default') 133 | 134 | auth.setPublic() 135 | t.ok(auth.isPublic(), 'setPublic() results in public access') 136 | t.equal(auth.group, acl.EVERYONE) 137 | t.notOk(auth.agent) 138 | 139 | auth = new Authorization() 140 | auth.setGroup(acl.EVERYONE) 141 | t.ok(auth.isPublic(), 142 | 'Adding group access to everyone should result in public access') 143 | t.ok(auth.group, 'Public access authorization is a group authorization') 144 | t.notOk(auth.agent, 'A public access auth should have a null agent') 145 | 146 | auth = new Authorization() 147 | auth.setAgent(acl.EVERYONE) 148 | t.ok(auth.isPublic(), 149 | 'Setting the agent to everyone should be the same as setPublic()') 150 | t.end() 151 | }) 152 | 153 | test('an webId is either the agent or the group id', function (t) { 154 | let auth = new Authorization() 155 | auth.setAgent(agentWebId) 156 | t.equal(auth.webId(), auth.agent) 157 | auth = new Authorization() 158 | auth.setGroup(groupWebId) 159 | t.equal(auth.webId(), auth.group) 160 | t.end() 161 | }) 162 | 163 | test('hashFragment() on an incomplete authorization should fail', function (t) { 164 | const auth = new Authorization() 165 | t.throws(function () { 166 | auth.hashFragment() 167 | }, 'hashFragment() should fail if both webId AND resourceUrl are missing') 168 | auth.setAgent(agentWebId) 169 | t.throws(function () { 170 | auth.hashFragment() 171 | }, 'hashFragment() should fail if either webId OR resourceUrl are missing') 172 | t.end() 173 | }) 174 | 175 | test('Authorization.isValid() test', function (t) { 176 | const auth = new Authorization() 177 | t.notOk(auth.isValid(), 'An empty authorization should not be valid') 178 | auth.resourceUrl = resourceUrl 179 | t.notOk(auth.isValid()) 180 | auth.setAgent(agentWebId) 181 | t.notOk(auth.isValid()) 182 | auth.addMode(acl.READ) 183 | t.ok(auth.isValid()) 184 | auth.agent = null 185 | auth.setGroup(groupWebId) 186 | t.ok(auth.isValid()) 187 | t.end() 188 | }) 189 | 190 | test('Authorization origins test', function (t) { 191 | const auth = new Authorization() 192 | const origin = 'https://example.com/' 193 | auth.addOrigin(origin) 194 | t.deepEqual(auth.allOrigins(), [origin]) 195 | t.ok(auth.allowsOrigin(origin)) 196 | auth.removeOrigin(origin) 197 | t.deepEqual(auth.allOrigins(), []) 198 | t.notOk(auth.allowsOrigin(origin)) 199 | t.end() 200 | }) 201 | 202 | test('Comparing Authorizations test 1', function (t) { 203 | const auth1 = new Authorization() 204 | const auth2 = new Authorization() 205 | t.ok(auth1.equals(auth2)) 206 | t.end() 207 | }) 208 | 209 | test('Comparing Authorizations test 2', function (t) { 210 | const auth1 = new Authorization(resourceUrl) 211 | const auth2 = new Authorization() 212 | t.notOk(auth1.equals(auth2)) 213 | auth2.resourceUrl = resourceUrl 214 | t.ok(auth1.equals(auth2)) 215 | t.end() 216 | }) 217 | 218 | test('Comparing Authorizations test 3', function (t) { 219 | const auth1 = new Authorization() 220 | auth1.setAgent(agentWebId) 221 | const auth2 = new Authorization() 222 | t.notOk(auth1.equals(auth2)) 223 | auth2.setAgent(agentWebId) 224 | t.ok(auth1.equals(auth2)) 225 | t.end() 226 | }) 227 | 228 | test('Comparing Authorizations test 4', function (t) { 229 | const auth1 = new Authorization() 230 | auth1.addMode([acl.READ, acl.WRITE]) 231 | const auth2 = new Authorization() 232 | t.notOk(auth1.equals(auth2)) 233 | auth2.addMode([acl.READ, acl.WRITE]) 234 | t.ok(auth1.equals(auth2)) 235 | t.end() 236 | }) 237 | 238 | test('Comparing Authorizations test 5', function (t) { 239 | const auth1 = new Authorization(resourceUrl, acl.INHERIT) 240 | const auth2 = new Authorization(resourceUrl) 241 | t.notOk(auth1.equals(auth2)) 242 | auth2.inherited = acl.INHERIT 243 | t.ok(auth1.equals(auth2)) 244 | t.end() 245 | }) 246 | 247 | test('Comparing Authorizations test 6', function (t) { 248 | const auth1 = new Authorization() 249 | auth1.addMailTo('alice@example.com') 250 | const auth2 = new Authorization() 251 | t.notOk(auth1.equals(auth2)) 252 | auth2.addMailTo('alice@example.com') 253 | t.ok(auth1.equals(auth2)) 254 | t.end() 255 | }) 256 | 257 | test('Comparing Authorizations test 7', function (t) { 258 | const origin = 'https://example.com/' 259 | const auth1 = new Authorization() 260 | auth1.addOrigin(origin) 261 | const auth2 = new Authorization() 262 | t.notOk(auth1.equals(auth2)) 263 | auth2.addOrigin(origin) 264 | t.ok(auth1.equals(auth2)) 265 | t.end() 266 | }) 267 | 268 | test('Authorization.clone() test', function (t) { 269 | const auth1 = new Authorization(resourceUrl, acl.INHERIT) 270 | auth1.addMode([acl.READ, acl.WRITE]) 271 | const auth2 = auth1.clone() 272 | t.ok(auth1.equals(auth2)) 273 | t.end() 274 | }) 275 | -------------------------------------------------------------------------------- /test/unit/check-access-test.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const test = require('tape') 4 | // const Authorization = require('../../src/authorization') 5 | // const { acl } = require('../../src/modes') 6 | // const PermissionSet = require('../../src/permission-set') 7 | const aclLogic = require('../../src/acl-check') 8 | const $rdf = require('rdflib') 9 | 10 | const ACL = $rdf.Namespace('http://www.w3.org/ns/auth/acl#') 11 | const FOAF = $rdf.Namespace('http://xmlns.com/foaf/0.1/') 12 | const ALICE = $rdf.Namespace('https://alice.example.com/') 13 | 14 | const prefixes = `@prefix acl: . 15 | @prefix foaf: . 16 | @prefix alice: . 17 | @prefix bob: . 18 | ` 19 | const alice = $rdf.sym('https://alice.example.com/#me') 20 | const bob = $rdf.sym('https://bob.example.com/#me') 21 | const malory = $rdf.sym('https://someone.else.example.com/') 22 | 23 | // Append access implied by Write acecss 24 | test('aclCheck checkAccess() test - Append access implied by Write acecss', t => { 25 | const resource = $rdf.sym('https://alice.example.com/docs/file1') 26 | const aclUrl = 'https://alice.example.com/docs/.acl' 27 | const aclDoc = $rdf.sym(aclUrl) 28 | 29 | const store = $rdf.graph() // Quad store 30 | const ACLtext = prefixes + 31 | ` <#auth> a acl:Authorization; 32 | acl:mode acl:Write; 33 | acl:agent alice:me; 34 | acl:accessTo <${resource.uri}> . 35 | ` 36 | $rdf.parse(ACLtext, store, aclUrl, 'text/turtle') 37 | 38 | const agent = alice 39 | const directory = null 40 | const modesRequired = [ACL('Append')] 41 | const trustedOrigins = null 42 | const origin = null 43 | 44 | const result = aclLogic.checkAccess(store, resource, directory, aclDoc, agent, modesRequired, origin, trustedOrigins) 45 | t.ok(result, 'Alice should have Append access implied by Write access') 46 | t.end() 47 | }) 48 | 49 | // Straight ACL access test 50 | test('acl-check checkAccess() test - accessTo', function (t) { 51 | const container = $rdf.sym('https://alice.example.com/docs/') 52 | const containerAclUrl = 'https://alice.example.com/docs/.acl' 53 | const containerAcl = $rdf.sym(containerAclUrl) 54 | 55 | const store = $rdf.graph() // Quad store 56 | const ACLtext = prefixes + 57 | ` <#auth> a acl:Authorization; 58 | acl:mode acl:Read, acl:Write; 59 | acl:agent alice:me; 60 | acl:accessTo <${container.uri}> . 61 | ` 62 | $rdf.parse(ACLtext, store, containerAclUrl, 'text/turtle') 63 | 64 | var result = aclLogic.checkAccess(store, container, null, containerAcl, alice, [ACL('Read')]) 65 | t.ok(result, 'Alice should have Read acces') 66 | 67 | result = aclLogic.checkAccess(store, container, null, containerAcl, alice, [ACL('Write')]) 68 | t.ok(result, 'Alice should have Write acces') 69 | 70 | result = !aclLogic.checkAccess(store, container, null, containerAcl, bob, [ACL('Write')]) 71 | t.ok(result, 'Bob Should not have access') 72 | 73 | t.end() 74 | }) 75 | 76 | // Inheriting permissions from directory defaults 77 | test('acl-check checkAccess() test - default/inherited', function (t) { 78 | const container = $rdf.sym('https://alice.example.com/docs/') 79 | const containerAcl = $rdf.sym('https://alice.example.com/docs/.acl') 80 | const file1 = $rdf.sym('https://alice.example.com/docs/file1') 81 | const file2 = $rdf.sym('https://alice.example.com/docs/stuff/file2') 82 | var result 83 | const store = $rdf.graph() 84 | /* 85 | let ACLtext = prefixes + ` <#auth> a acl:Authorization; 86 | acl:mode acl:Read; 87 | acl:agent bob:me; 88 | acl:accessTo <${file1.uri}> . 89 | ` 90 | $rdf.parse(ACLtext, store, containerAcl.uri, 'text/turtle') 91 | */ 92 | const containerAclText = prefixes + ` <#auth> a acl:Authorization; 93 | acl:mode acl:Read; 94 | acl:agent alice:me; 95 | acl:default <${container.uri}> . 96 | ` 97 | $rdf.parse(containerAclText, store, containerAcl.uri, 'text/turtle') 98 | 99 | result = aclLogic.checkAccess(store, file1, container, containerAcl, alice, [ACL('Read')]) 100 | t.ok(result, 'Alice should have Read access inherited') 101 | 102 | result = aclLogic.checkAccess(store, file2, container, containerAcl, alice, [ACL('Read')]) 103 | t.ok(result, 'Alice should have Read access inherited 2') 104 | 105 | result = !aclLogic.checkAccess(store, file2, container, containerAcl, alice, [ACL('Write')]) 106 | t.ok(result, 'Alice should NOT have Write access inherited') 107 | 108 | t.end() 109 | }) 110 | 111 | // Inheriting permissions from directory defaults -- OLD version defaultForNew 112 | test('acl-check checkAccess() test - default/inherited', function (t) { 113 | const container = $rdf.sym('https://alice.example.com/docs/') 114 | const containerAcl = $rdf.sym('https://alice.example.com/docs/.acl') 115 | const file1 = $rdf.sym('https://alice.example.com/docs/file1') 116 | const file2 = $rdf.sym('https://alice.example.com/docs/stuff/file2') 117 | var result 118 | const store = $rdf.graph() 119 | /* 120 | let ACLtext = prefixes + ` <#auth> a acl:Authorization; 121 | acl:mode acl:Read; 122 | acl:agent bob:me; 123 | acl:accessTo <${file1.uri}> . 124 | ` 125 | $rdf.parse(ACLtext, store, containerAcl.uri, 'text/turtle') 126 | */ 127 | const containerAclText = prefixes + ` <#auth> a acl:Authorization; 128 | acl:mode acl:Read; 129 | acl:agent alice:me; 130 | acl:defaultForNew <${container.uri}> . 131 | ` 132 | $rdf.parse(containerAclText, store, containerAcl.uri, 'text/turtle') 133 | 134 | result = aclLogic.checkAccess(store, file1, container, containerAcl, alice, [ACL('Read')]) 135 | t.ok(result, 'Alice should have Read access inherited') 136 | 137 | result = aclLogic.checkAccess(store, file2, container, containerAcl, alice, [ACL('Read')]) 138 | t.ok(result, 'Alice should have Read access inherited 2') 139 | 140 | result = !aclLogic.checkAccess(store, file2, container, containerAcl, alice, [ACL('Write')]) 141 | t.ok(result, 'Alice should NOT have Write access inherited') 142 | 143 | result = !aclLogic.checkAccess(store, file2, container, containerAcl, alice, [ACL('Write'), ACL('Read')]) 144 | t.ok(result, 'Alice should NOT have Read and Write access inherited') 145 | 146 | t.end() 147 | }) 148 | 149 | /// ////////////////////////////////////// Public access VESRIONS OF THESE 150 | // Append access implied by Write acecss -PUBLIC 151 | test('aclCheck checkAccess() test - Append access implied by Public Write acecss', t => { 152 | const resource = $rdf.sym('https://alice.example.com/docs/file1') 153 | const aclUrl = 'https://alice.example.com/docs/.acl' 154 | const aclDoc = $rdf.sym(aclUrl) 155 | 156 | const store = $rdf.graph() // Quad store 157 | const ACLtext = prefixes + 158 | ` <#auth> a acl:Authorization; 159 | acl:mode acl:Write; 160 | acl:agentClass foaf:Agent; 161 | acl:accessTo <${resource.uri}> . 162 | ` 163 | $rdf.parse(ACLtext, store, aclUrl, 'text/turtle') 164 | 165 | const modesRequired = [ACL('Append')] 166 | 167 | const result = aclLogic.checkAccess(store, resource, null, aclDoc, alice, modesRequired) 168 | t.ok(result, 'Alice should have Append access implied by Write access - Public') 169 | 170 | t.end() 171 | }) 172 | 173 | // Straight ACL access test 174 | test('acl-check checkAccess() test - accessTo', function (t) { 175 | const container = $rdf.sym('https://alice.example.com/docs/') 176 | const containerAclUrl = 'https://alice.example.com/docs/.acl' 177 | const containerAcl = $rdf.sym(containerAclUrl) 178 | 179 | const store = $rdf.graph() // Quad store 180 | const ACLtext = prefixes + 181 | ` <#auth> a acl:Authorization; 182 | acl:mode acl:Read, acl:Write; 183 | acl:agentClass foaf:Agent; 184 | acl:accessTo <${container.uri}> . 185 | ` 186 | $rdf.parse(ACLtext, store, containerAclUrl, 'text/turtle') 187 | 188 | var result = aclLogic.checkAccess(store, container, null, containerAcl, alice, [ACL('Read')]) 189 | t.ok(result, 'Alice should have Read access - Public') 190 | 191 | result = aclLogic.checkAccess(store, container, null, containerAcl, alice, [ACL('Write')]) 192 | t.ok(result, 'Alice should have Write acces') 193 | 194 | var result = aclLogic.checkAccess(store, container, null, containerAcl, null, [ACL('Read')]) 195 | t.ok(result, 'Anonymous should have Read access to public thing - Public') 196 | 197 | result = aclLogic.checkAccess(store, container, null, containerAcl, null, [ACL('Write')]) 198 | t.ok(result, 'Anonymous should have Write access - Public') 199 | 200 | result = aclLogic.checkAccess(store, container, null, containerAcl, bob, [ACL('Write')]) 201 | t.ok(result, 'Bob should have Write access to public write - Public') 202 | 203 | t.end() 204 | }) 205 | 206 | // Inheriting permissions from directory defaults 207 | test('acl-check checkAccess() test - default/inherited', function (t) { 208 | const container = $rdf.sym('https://alice.example.com/docs/') 209 | const containerAcl = $rdf.sym('https://alice.example.com/docs/.acl') 210 | const file1 = $rdf.sym('https://alice.example.com/docs/file1') 211 | const file2 = $rdf.sym('https://alice.example.com/docs/stuff/file2') 212 | var result 213 | const store = $rdf.graph() 214 | /* 215 | let ACLtext = prefixes + ` <#auth> a acl:Authorization; 216 | acl:mode acl:Read; 217 | acl:agent bob:me; 218 | acl:accessTo <${file1.uri}> . 219 | ` 220 | $rdf.parse(ACLtext, store, containerAcl.uri, 'text/turtle') 221 | */ 222 | const containerAclText = prefixes + ` <#auth> a acl:Authorization; 223 | acl:mode acl:Read; 224 | acl:agentClass foaf:Agent; 225 | acl:default <${container.uri}> . 226 | ` 227 | $rdf.parse(containerAclText, store, containerAcl.uri, 'text/turtle') 228 | console.log('@@' + containerAclText + '@@@') 229 | result = aclLogic.checkAccess(store, file1, container, containerAcl, alice, [ACL('Read')]) 230 | t.ok(result, 'Alice should have Read access inherited - Public') 231 | 232 | result = aclLogic.checkAccess(store, file2, container, containerAcl, alice, [ACL('Read')]) 233 | t.ok(result, 'Alice should have Read access inherited 2 - Public') 234 | 235 | result = !aclLogic.checkAccess(store, file2, container, containerAcl, alice, [ACL('Write')]) 236 | t.ok(result, 'Alice should NOT have write access inherited - Public') 237 | 238 | t.end() 239 | }) 240 | 241 | /// ///////////////////////// Non-anonymoud versions 242 | // Append access implied by Write acecss -PUBLIC 243 | test('aclCheck checkAccess() test - Append access implied by Public Write acecss', t => { 244 | const resource = $rdf.sym('https://alice.example.com/docs/file1') 245 | const aclUrl = 'https://alice.example.com/docs/.acl' 246 | const aclDoc = $rdf.sym(aclUrl) 247 | 248 | const store = $rdf.graph() // Quad store 249 | const ACLtext = prefixes + 250 | ` <#auth> a acl:Authorization; 251 | acl:mode acl:Write; 252 | acl:agentClass acl:AuthenticatedAgent; 253 | acl:accessTo <${resource.uri}> . 254 | ` 255 | $rdf.parse(ACLtext, store, aclUrl, 'text/turtle') 256 | 257 | const modesRequired = [ACL('Append')] 258 | 259 | const result = aclLogic.checkAccess(store, resource, null, aclDoc, alice, modesRequired) 260 | t.ok(result, 'Alice should have Append access implied by Write access - AuthenticatedAgent') 261 | 262 | t.end() 263 | }) 264 | 265 | // Straight ACL access test 266 | test('acl-check checkAccess() test - accessTo', function (t) { 267 | const container = $rdf.sym('https://alice.example.com/docs/') 268 | const containerAclUrl = 'https://alice.example.com/docs/.acl' 269 | const containerAcl = $rdf.sym(containerAclUrl) 270 | 271 | const store = $rdf.graph() // Quad store 272 | const ACLtext = prefixes + 273 | ` <#auth> a acl:Authorization; 274 | acl:mode acl:Read, acl:Write; 275 | acl:agentClass acl:AuthenticatedAgent; 276 | acl:accessTo <${container.uri}> . 277 | ` 278 | $rdf.parse(ACLtext, store, containerAclUrl, 'text/turtle') 279 | 280 | var result = aclLogic.checkAccess(store, container, null, containerAcl, alice, [ACL('Read')]) 281 | t.ok(result, 'Alice should have Read access - AuthenticatedAgent') 282 | 283 | result = aclLogic.checkAccess(store, container, null, containerAcl, alice, [ACL('Write')]) 284 | t.ok(result, 'Alice should have Write acces') 285 | 286 | var result = !aclLogic.checkAccess(store, container, null, containerAcl, null, [ACL('Read')]) 287 | t.ok(result, 'Anonymous should NOT have Read access to public thing - AuthenticatedAgent') 288 | 289 | result = !aclLogic.checkAccess(store, container, null, containerAcl, null, [ACL('Write')]) 290 | t.ok(result, 'Anonymous should NOT have Write access - AuthenticatedAgent') 291 | 292 | result = aclLogic.checkAccess(store, container, null, containerAcl, bob, [ACL('Write')]) 293 | t.ok(result, 'Bob should have Write access to public write - AuthenticatedAgent') 294 | 295 | t.end() 296 | }) 297 | 298 | // Inheriting permissions from directory defaults 299 | test('acl-check checkAccess() test - default/inherited', function (t) { 300 | const container = $rdf.sym('https://alice.example.com/docs/') 301 | const containerAcl = $rdf.sym('https://alice.example.com/docs/.acl') 302 | const file1 = $rdf.sym('https://alice.example.com/docs/file1') 303 | const file2 = $rdf.sym('https://alice.example.com/docs/stuff/file2') 304 | var result 305 | const store = $rdf.graph() 306 | const ACLtext = prefixes + ` <#auth> a acl:Authorization; 307 | acl:mode acl:Read; 308 | acl:agent bob:me; 309 | acl:accessTo <${file1.uri}> . 310 | ` 311 | $rdf.parse(ACLtext, store, containerAcl.uri, 'text/turtle') 312 | 313 | const containerAclText = prefixes + ` <#auth> a acl:Authorization; 314 | acl:mode acl:Read; 315 | acl:agentClass acl:AuthenticatedAgent; 316 | acl:default <${container.uri}> . 317 | ` 318 | $rdf.parse(containerAclText, store, containerAcl.uri, 'text/turtle') 319 | 320 | result = aclLogic.checkAccess(store, file1, container, containerAcl, alice, [ACL('Read')]) 321 | t.ok(result, 'Alice should have Read access inherited - AuthenticatedAgent') 322 | 323 | result = aclLogic.checkAccess(store, file2, container, containerAcl, alice, [ACL('Read')]) 324 | t.ok(result, 'Alice should have Read access inherited 2 - AuthenticatedAgent') 325 | 326 | result = !aclLogic.checkAccess(store, file2, container, containerAcl, alice, [ACL('Write')]) 327 | t.ok(result, 'Alice should NOT have write access inherited - AuthenticatedAgent') 328 | 329 | t.end() 330 | }) 331 | 332 | test('aclCheck checkAccess() test - with use of originTrustedModes', t => { 333 | const resource = ALICE('docs/file1') 334 | const aclDoc = ALICE('docs/.acl') 335 | const aclUrl = aclDoc.uri 336 | 337 | const origin = $rdf.sym('https://apps.example.com') 338 | const aclStore = $rdf.graph() 339 | // grants read, write and control access to Alice 340 | const ACLtext = `${prefixes} 341 | <#auth> a acl:Authorization; 342 | acl:mode acl:Read, acl:Write, acl:Control; 343 | acl:agent alice:me; 344 | acl:accessTo ${resource} . 345 | ` 346 | $rdf.parse(ACLtext, aclStore, aclUrl, 'text/turtle') 347 | 348 | const agent = alice 349 | const directory = null 350 | const trustedOrigins = [] 351 | const originTrustedModes = [ACL('Read'), ACL('Write')] 352 | 353 | const readWriteModeRequired = [ACL('Read'), ACL('Write')] 354 | const readWriteModeResult = aclLogic.checkAccess(aclStore, resource, directory, aclDoc, agent, readWriteModeRequired, origin, trustedOrigins, originTrustedModes) 355 | t.ok(readWriteModeResult, 'Should get access to modes when origin is listed as trusted app') 356 | 357 | const controlModeRequired = [ACL('Control')] 358 | const controlModeResult = aclLogic.checkAccess(aclStore, resource, directory, aclDoc, agent, controlModeRequired, origin, trustedOrigins, originTrustedModes) 359 | t.ok(!controlModeResult, 'All Required Access Modes Not Granted', 'Correct reason') 360 | 361 | t.end() 362 | }) 363 | -------------------------------------------------------------------------------- /test/unit/clear-permissions-test.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const test = require('tape') 4 | const SolidResponse = require('solid-web-client/lib/models/response') 5 | const acls = require('../../src/index') // solid-permissions module 6 | const resourceUrl = 'https://example.com/resource1' 7 | const sinon = require('sinon') 8 | const deleteSpy = sinon.spy() 9 | 10 | const mockWebClient = { 11 | head: (url) => { 12 | const response = new SolidResponse() 13 | response.url = url 14 | response.acl = 'resource1.acl' 15 | return Promise.resolve(response) 16 | }, 17 | del: deleteSpy 18 | } 19 | 20 | test('clearPermissions() test', t => { 21 | const aclUrl = 'https://example.com/resource1.acl' 22 | acls.clearPermissions(resourceUrl, mockWebClient) 23 | .then(() => { 24 | t.ok(deleteSpy.calledWith(aclUrl), 25 | 'should result in a DELETE call to the .acl url') 26 | t.end() 27 | }) 28 | .catch(err => { 29 | console.log(err) 30 | t.fail() 31 | }) 32 | }) 33 | -------------------------------------------------------------------------------- /test/unit/configure-logger-test.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const test = require('tape') 4 | const sinon = require('sinon') 5 | const { log, configureLogger } = require('../../src/acl-check') 6 | 7 | test('by default logger is console.log', t => { 8 | const defaultLogger = console.log 9 | console.log = sinon.spy() 10 | 11 | log('foo', 'bar', 42) 12 | 13 | t.ok(console.log.calledWith('foo', 'bar', 42), 'should call console.log by default') 14 | 15 | t.end() 16 | 17 | console.log = defaultLogger 18 | }) 19 | 20 | test('can set custom logger', t => { 21 | const logger = sinon.stub() 22 | configureLogger(logger) 23 | 24 | log('foo', 'bar', 42) 25 | 26 | t.ok(logger.calledWith('foo', 'bar', 42), 'should call custom logger') 27 | 28 | t.end() 29 | 30 | configureLogger(null) 31 | }) 32 | -------------------------------------------------------------------------------- /test/unit/get-permissions-test.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const test = require('tape') 4 | const rdf = require('rdflib') 5 | const PermissionSet = require('../../src/permission-set') 6 | const SolidResponse = require('solid-web-client/lib/models/response') 7 | 8 | const acls = require('../../src/index') // solid-permissions module 9 | 10 | const resourceUrl = 'https://example.com/resource1' 11 | const webId = 'https://example.com/#me' 12 | 13 | const aclSource = `@prefix acl: . 14 | @prefix foaf: . 15 | <#authorization> 16 | acl:agent ; 17 | acl:accessTo ; 18 | acl:mode acl:Read, acl:Write, acl:Control.` 19 | 20 | const mockWebClient = { 21 | head: (url) => { 22 | const response = new SolidResponse() 23 | response.rdf = rdf 24 | response.url = url 25 | response.acl = 'resource1.acl' 26 | return Promise.resolve(response) 27 | }, 28 | get: (url) => { 29 | const response = new SolidResponse() 30 | response.rdf = rdf 31 | response.url = url 32 | response.xhr = { 33 | response: aclSource 34 | } 35 | response.contentType = () => { return 'text/turtle' } 36 | return Promise.resolve(response) 37 | } 38 | } 39 | 40 | test('getPermissions() test', t => { 41 | acls.getPermissions(resourceUrl, mockWebClient, rdf) 42 | .then(permissionSet => { 43 | t.ok(permissionSet instanceof PermissionSet, 44 | 'Result should be a PermissionSet instance') 45 | permissionSet.checkAccess(resourceUrl, webId, 'READ') 46 | .then(hasAccess => { 47 | t.ok(hasAccess, 'User should have READ access') 48 | t.end() 49 | }) 50 | }) 51 | .catch(err => { 52 | console.log(err) 53 | t.fail() 54 | }) 55 | }) 56 | -------------------------------------------------------------------------------- /test/unit/get-trusted-modes-for-origin-test.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const test = require('tape') 4 | const aclLogic = require('../../src/acl-check') 5 | const $rdf = require('rdflib') 6 | 7 | const ACL = $rdf.Namespace('http://www.w3.org/ns/auth/acl#') 8 | const ALICE = $rdf.Namespace('https://alice.example.com/') 9 | const alice = ALICE('#me') 10 | const BOB = $rdf.Namespace('https://bob.example.com/') 11 | const bob = BOB('#me') 12 | 13 | const prefixes = ` 14 | @prefix acl: ${ACL()} . 15 | @prefix alice: ${ALICE('#')} . 16 | ` 17 | 18 | test('aclCheck getTrustedModesForOrigin() getting trusted modes from publisherStore (acl:accessTo on resource)', t => { 19 | const origin = $rdf.sym('https://apps.example.com') 20 | const doc = ALICE('some/doc.txt') 21 | const aclDoc = ALICE('some/doc.txt.acl') 22 | const publisher = alice 23 | const requester = bob 24 | const publisherStore = $rdf.graph() 25 | const aclFileText = `${prefixes} 26 | <#owner> 27 | a acl:Authorization; 28 | acl:agent ${publisher}; 29 | acl:accessTo ${doc}; 30 | acl:mode acl:Control. 31 | ` 32 | $rdf.parse(aclFileText, publisherStore, aclDoc.uri, 'text/turtle') 33 | const publisherText = `${prefixes} 34 | ${publisher} acl:trustedApp [ acl:origin ${origin}; 35 | acl:mode acl:Read, acl:Write]. 36 | ` 37 | $rdf.parse(publisherText, publisherStore, publisher.uri, 'text/turtle') 38 | 39 | aclLogic.getTrustedModesForOrigin(publisherStore, doc, null, aclDoc, origin, Promise.resolve.bind(Promise)).then(result => { 40 | t.deepEqual(result, [ACL('Read'), ACL('Write')], 'Should get a list of modes') 41 | t.end() 42 | }) 43 | }) 44 | 45 | test('aclCheck getTrustedModesForOrigin() getting trusted modes from publisherStore (acl:accessTo on container)', t => { 46 | const origin = $rdf.sym('https://apps.example.com') 47 | const container = ALICE('some/') 48 | const doc = ALICE('some/doc.txt') 49 | const aclDoc = ALICE('some/doc.txt.acl') 50 | const publisher = alice 51 | const requester = bob 52 | const publisherStore = $rdf.graph() 53 | const aclFileText = `${prefixes} 54 | <#owner> 55 | a acl:Authorization; 56 | acl:agent ${publisher}; 57 | acl:default ${container}; 58 | acl:mode acl:Control. 59 | ` 60 | $rdf.parse(aclFileText, publisherStore, aclDoc.uri, 'text/turtle') 61 | const publisherText = `${prefixes} 62 | ${publisher} acl:trustedApp [ acl:origin ${origin}; 63 | acl:mode acl:Read, acl:Write]. 64 | ` 65 | $rdf.parse(publisherText, publisherStore, publisher.uri, 'text/turtle') 66 | 67 | aclLogic.getTrustedModesForOrigin(publisherStore, doc, container, aclDoc, origin, Promise.resolve.bind(Promise)).then(result => { 68 | t.deepEqual(result, [ACL('Read'), ACL('Write')], 'Should get a list of modes') 69 | t.end() 70 | }) 71 | }) 72 | -------------------------------------------------------------------------------- /test/unit/module-index-test.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | const test = require('tape') 3 | const acls = require('../../src/index') 4 | 5 | test('Module exports test', t => { 6 | t.ok(acls.ALL_MODES) 7 | t.ok(acls.READ && acls.WRITE && acls.APPEND && acls.CONTROL) 8 | t.ok(acls.EVERYONE) 9 | t.ok(acls.INHERIT) 10 | t.ok(acls.ACCESS_TO) 11 | t.ok(acls.DEFAULT) 12 | t.ok(acls.getPermissions) 13 | t.ok(acls.clearPermissions) 14 | t.ok(acls.PermissionSet) 15 | t.ok(acls.Authorization) 16 | t.end() 17 | }) 18 | -------------------------------------------------------------------------------- /test/unit/permission-set-test.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const test = require('tape') 4 | const before = test 5 | const sinon = require('sinon') 6 | const rdf = require('rdflib') 7 | const Authorization = require('../../src/authorization') 8 | const { acl } = require('../../src/modes') 9 | const PermissionSet = require('../../src/permission-set') 10 | 11 | const resourceUrl = 'https://alice.example.com/docs/file1' 12 | const aclUrl = 'https://alice.example.com/docs/file1.acl' 13 | const containerUrl = 'https://alice.example.com/docs/' 14 | const containerAclUrl = 'https://alice.example.com/docs/.acl' 15 | const bobWebId = 'https://bob.example.com/#me' 16 | const aliceWebId = 'https://alice.example.com/#me' 17 | // Not really sure what group webIDs will look like, not yet implemented: 18 | const groupWebId = 'https://devteam.example.com/something' 19 | 20 | function parseGraph (rdf, baseUrl, rdfSource, contentType = 'text/turtle') { 21 | const graph = rdf.graph() 22 | return new Promise((resolve, reject) => { 23 | rdf.parse(rdfSource, graph, baseUrl, contentType, (err, result) => { 24 | if (err) { return reject(err) } 25 | if (!result) { 26 | return reject(new Error('Error serializing the graph to ' + 27 | contentType)) 28 | } 29 | resolve(result) 30 | }) 31 | }) 32 | } 33 | const rawAclSource = require('../resources/acl-container-ttl') 34 | var parsedAclGraph 35 | 36 | before('init graph', t => { 37 | return parseGraph(rdf, aclUrl, rawAclSource) 38 | .then(graph => { 39 | parsedAclGraph = graph 40 | t.end() 41 | }) 42 | .catch(err => { 43 | t.fail(err) 44 | }) 45 | }) 46 | 47 | test('a new PermissionSet()', function (t) { 48 | const ps = new PermissionSet() 49 | t.ok(ps.isEmpty(), 'should be empty') 50 | t.equal(ps.count, 0, 'should have a count of 0') 51 | t.notOk(ps.resourceUrl, 'should have a null resource url') 52 | t.notOk(ps.aclUrl, 'should have a null acl url') 53 | t.end() 54 | }) 55 | 56 | test('a new PermissionSet() for a resource', function (t) { 57 | const ps = new PermissionSet(resourceUrl) 58 | t.ok(ps.isEmpty(), 'should be empty') 59 | t.equal(ps.count, 0, 'should have a count of 0') 60 | t.equal(ps.resourceUrl, resourceUrl) 61 | t.notOk(ps.aclUrl, 'An acl url should be set explicitly') 62 | t.equal(ps.resourceType, PermissionSet.RESOURCE, 63 | 'A permission set should be for a resource by default (not container)') 64 | t.end() 65 | }) 66 | 67 | test('PermissionSet can add and remove agent authorizations', function (t) { 68 | const ps = new PermissionSet(resourceUrl, aclUrl) 69 | t.equal(ps.aclUrl, aclUrl) 70 | const origin = 'https://example.com/' 71 | // Notice that addPermission() is chainable: 72 | ps 73 | .addPermission(bobWebId, acl.READ, origin) // only allow read from origin 74 | .addPermission(aliceWebId, [acl.READ, acl.WRITE]) 75 | t.notOk(ps.isEmpty()) 76 | t.equal(ps.count, 2) 77 | let auth = ps.permissionFor(bobWebId) 78 | t.equal(auth.agent, bobWebId) 79 | t.equal(auth.resourceUrl, resourceUrl) 80 | t.equal(auth.resourceType, Authorization.RESOURCE) 81 | t.ok(auth.allowsOrigin(origin)) 82 | t.ok(auth.allowsRead()) 83 | t.notOk(auth.allowsWrite()) 84 | // adding further permissions for an existing agent just merges access modes 85 | ps.addPermission(bobWebId, acl.WRITE) 86 | // should still only be 2 authorizations 87 | t.equal(ps.count, 2) 88 | auth = ps.permissionFor(bobWebId) 89 | t.ok(auth.allowsWrite()) 90 | 91 | // Now remove the added permission 92 | ps.removePermission(bobWebId, acl.READ) 93 | // Still 2 authorizations, agent1 has a WRITE permission remaining 94 | t.equal(ps.count, 2) 95 | auth = ps.permissionFor(bobWebId) 96 | t.notOk(auth.allowsRead()) 97 | t.ok(auth.allowsWrite()) 98 | 99 | // Now, if you remove the remaining WRITE permission from agent1, that whole 100 | // authorization is removed 101 | ps.removePermission(bobWebId, acl.WRITE) 102 | t.equal(ps.count, 1, 'Only one authorization should remain') 103 | t.notOk(ps.permissionFor(bobWebId), 104 | 'No authorization for agent1 should be found') 105 | t.end() 106 | }) 107 | 108 | test('PermissionSet no duplicate authorizations test', function (t) { 109 | const ps = new PermissionSet(resourceUrl, aclUrl) 110 | // Now add two identical permissions 111 | ps.addPermission(aliceWebId, [acl.READ, acl.WRITE]) 112 | ps.addPermission(aliceWebId, [acl.READ, acl.WRITE]) 113 | t.equal(ps.count, 1, 'Duplicate authorizations should be eliminated') 114 | t.end() 115 | }) 116 | 117 | test('PermissionSet can add and remove group authorizations', function (t) { 118 | const ps = new PermissionSet(resourceUrl) 119 | // Let's add an agentGroup permission 120 | ps.addGroupPermission(groupWebId, [acl.READ, acl.WRITE]) 121 | t.equal(ps.count, 1) 122 | const auth = ps.permissionFor(groupWebId) 123 | t.equal(auth.group, groupWebId) 124 | ps.removePermission(groupWebId, [acl.READ, acl.WRITE]) 125 | t.ok(ps.isEmpty()) 126 | t.end() 127 | }) 128 | 129 | test('iterating over a PermissionSet', function (t) { 130 | const ps = new PermissionSet(resourceUrl, aclUrl) 131 | ps 132 | .addPermission(bobWebId, acl.READ) 133 | .addPermission(aliceWebId, [acl.READ, acl.WRITE]) 134 | ps.forEach(function (auth) { 135 | t.ok(auth.hashFragment() in ps.authorizations) 136 | }) 137 | t.end() 138 | }) 139 | 140 | test.skip('a PermissionSet() for a container', function (t) { 141 | const isContainer = true 142 | const ps = new PermissionSet(containerUrl, aclUrl, isContainer) 143 | t.ok(ps.isAuthInherited(), 144 | 'A PermissionSet for a container should be inherited by default') 145 | ps.addPermission(bobWebId, acl.READ) 146 | const auth = ps.permissionFor(bobWebId) 147 | t.ok(auth.isInherited(), 148 | 'An authorization intended for a container should be inherited by default') 149 | t.end() 150 | }) 151 | 152 | test('a PermissionSet() for a resource (not container)', function (t) { 153 | const ps = new PermissionSet(containerUrl) 154 | t.notOk(ps.isAuthInherited()) 155 | ps.addPermission(bobWebId, acl.READ) 156 | const auth = ps.permissionFor(bobWebId) 157 | t.notOk(auth.isInherited(), 158 | 'An authorization intended for a resource should not be inherited by default') 159 | t.end() 160 | }) 161 | 162 | test('a PermissionSet can be initialized from an .acl graph', function (t) { 163 | const isContainer = false 164 | // see test/resources/acl-container-ttl.js 165 | const ps = new PermissionSet(resourceUrl, aclUrl, isContainer, 166 | { graph: parsedAclGraph, rdf }) 167 | 168 | // Check to make sure Alice's authorizations were read in correctly 169 | const auth = ps.findAuthByAgent(aliceWebId, resourceUrl) 170 | t.ok(auth, 'Alice should have a permission for /docs/file1') 171 | t.ok(auth.isInherited()) 172 | t.ok(auth.allowsWrite() && auth.allowsWrite() && auth.allowsControl()) 173 | // Check to make sure the acl:origin objects were read in 174 | t.ok(auth.allowsOrigin('https://example.com/')) 175 | // Check to make sure the `mailto:` agent objects were read in 176 | // This is @private / unofficial functionality, used only in the root ACL 177 | t.ok(auth.mailTo.length > 0, 'Alice agent should have a mailto: set') 178 | t.equal(auth.mailTo[0], 'alice@example.com') 179 | t.equal(auth.mailTo[1], 'bob@example.com') 180 | // Check to make sure Bob's authorizations were read in correctly 181 | const auth2 = ps.findAuthByAgent(bobWebId, resourceUrl) 182 | t.ok(auth2, 'Container acl should also have an authorization for Bob') 183 | t.ok(auth2.isInherited()) 184 | t.ok(auth2.allowsWrite() && auth2.allowsWrite() && auth2.allowsControl()) 185 | t.ok(auth2.mailTo.length > 0, 'Bob agent should have a mailto: set') 186 | t.equal(auth2.mailTo[0], 'alice@example.com') 187 | t.equal(auth2.mailTo[1], 'bob@example.com') 188 | // // Now check that the Public Read authorization was parsed 189 | const publicResource = 'https://alice.example.com/profile/card' 190 | const publicAuth = ps.findPublicAuth(publicResource) 191 | t.ok(publicAuth.isPublic()) 192 | t.notOk(publicAuth.isInherited()) 193 | t.ok(publicAuth.allowsRead()) 194 | t.end() 195 | }) 196 | 197 | test('PermissionSet equals test 1', function (t) { 198 | const ps1 = new PermissionSet() 199 | const ps2 = new PermissionSet() 200 | t.ok(ps1.equals(ps2)) 201 | t.end() 202 | }) 203 | 204 | test('PermissionSet equals test 2', function (t) { 205 | const ps1 = new PermissionSet(resourceUrl) 206 | const ps2 = new PermissionSet() 207 | t.notOk(ps1.equals(ps2)) 208 | ps2.resourceUrl = resourceUrl 209 | t.ok(ps1.equals(ps2)) 210 | 211 | ps1.aclUrl = aclUrl 212 | t.notOk(ps1.equals(ps2)) 213 | ps2.aclUrl = aclUrl 214 | t.ok(ps1.equals(ps2)) 215 | t.end() 216 | }) 217 | 218 | test('PermissionSet equals test 3', function (t) { 219 | const ps1 = new PermissionSet(containerUrl, containerAclUrl, 220 | PermissionSet.CONTAINER) 221 | const ps2 = new PermissionSet(containerUrl, containerAclUrl) 222 | t.notOk(ps1.equals(ps2)) 223 | ps2.resourceType = PermissionSet.CONTAINER 224 | t.ok(ps1.equals(ps2)) 225 | t.end() 226 | }) 227 | 228 | test('PermissionSet equals test 4', function (t) { 229 | const ps1 = new PermissionSet(resourceUrl) 230 | ps1.addPermission(aliceWebId, acl.READ) 231 | const ps2 = new PermissionSet(resourceUrl) 232 | t.notOk(ps1.equals(ps2)) 233 | ps2.addPermission(aliceWebId, acl.READ) 234 | t.ok(ps1.equals(ps2)) 235 | t.end() 236 | }) 237 | 238 | test('PermissionSet serialized & deserialized round trip test', function (t) { 239 | var ps = new PermissionSet(containerUrl, containerAclUrl, 240 | PermissionSet.CONTAINER, { graph: parsedAclGraph, rdf }) 241 | const auth = ps.permissionFor(aliceWebId) 242 | // console.log(ps.serialize()) 243 | t.ok(ps.equals(ps), 'A PermissionSet should equal itself') 244 | // Now check to make sure serialize() & reparse results in the same set 245 | return ps.serialize() 246 | .then((serializedTurtle) => { 247 | // Now that the PermissionSet is serialized to a Turtle string, 248 | // let's re-parse that string into a new graph 249 | return parseGraph(rdf, containerAclUrl, serializedTurtle) 250 | }) 251 | .then(parsedGraph => { 252 | const ps2 = new PermissionSet(containerUrl, containerAclUrl, 253 | PermissionSet.CONTAINER, { graph: parsedGraph, rdf }) 254 | // console.log(ps2.serialize()) 255 | t.ok(ps.equals(ps2), 256 | 'A PermissionSet serialized and re-parsed should equal the original one') 257 | t.end() 258 | }) 259 | }) 260 | 261 | test('PermissionSet allowsPublic() test', function (t) { 262 | var ps = new PermissionSet(containerUrl, containerAclUrl, 263 | PermissionSet.CONTAINER, { graph: parsedAclGraph, rdf }) 264 | const otherUrl = 'https://alice.example.com/profile/card' 265 | t.ok(ps.allowsPublic(acl.READ, otherUrl), 266 | 'Alice\'s profile should be public-readable') 267 | t.notOk(ps.allowsPublic(acl.WRITE, otherUrl), 268 | 'Alice\'s profile should not be public-writable') 269 | t.end() 270 | }) 271 | 272 | test('allowsPublic() should ignore origin checking', function (t) { 273 | const origin = 'https://example.com' 274 | const options = { graph: parsedAclGraph, rdf, origin, strictOrigin: true } 275 | var ps = new PermissionSet(containerUrl, containerAclUrl, 276 | PermissionSet.CONTAINER, options) 277 | const otherUrl = 'https://alice.example.com/profile/card' 278 | t.ok(ps.allowsPublic(acl.READ, otherUrl)) 279 | 280 | ps.checkAccess(otherUrl, 'https://alice.example.com', acl.READ) 281 | .then(hasAccess => { 282 | t.ok(hasAccess) 283 | t.end() 284 | }) 285 | }) 286 | 287 | test('PermissionSet init from untyped ACL test', function (t) { 288 | const rawAclSource = require('../resources/untyped-acl-ttl') 289 | const resourceUrl = 'https://alice.example.com/docs/file1' 290 | const aclUrl = 'https://alice.example.com/docs/file1.acl' 291 | const isContainer = false 292 | parseGraph(rdf, aclUrl, rawAclSource) 293 | .then(graph => { 294 | const ps = new PermissionSet(resourceUrl, aclUrl, isContainer, 295 | { graph, rdf }) 296 | t.ok(ps.count, 297 | 'Permission set should init correctly without acl:Authorization type') 298 | t.end() 299 | }) 300 | }) 301 | 302 | test('PermissionSet serialize() no rdf test', t => { 303 | const ps = new PermissionSet() 304 | ps.serialize() 305 | .then(() => { 306 | t.fail('Serialize should not succeed with no rdf lib') 307 | }) 308 | .catch(err => { 309 | t.equal(err.message, 'Cannot save - no rdf library') 310 | t.end() 311 | }) 312 | }) 313 | 314 | test('PermissionSet serialize() rdflib errors test', t => { 315 | const ps = new PermissionSet(resourceUrl, aclUrl, false, 316 | { rdf, graph: parsedAclGraph }) 317 | ps.serialize({ contentType: 'invalid' }) 318 | .then(() => { 319 | t.fail('Serialize should not succeed with an rdflib error') 320 | }) 321 | .catch(err => { 322 | t.ok(err.message.startsWith('Serialize: Content-type invalid')) 323 | t.end() 324 | }) 325 | }) 326 | 327 | test('PermissionSet save() test', t => { 328 | const resourceUrl = 'https://alice.example.com/docs/file1' 329 | const aclUrl = 'https://alice.example.com/docs/file1.acl' 330 | const isContainer = false 331 | const putStub = sinon.stub().returns(Promise.resolve()) 332 | const mockWebClient = { 333 | put: putStub 334 | } 335 | const ps = new PermissionSet(resourceUrl, aclUrl, isContainer, 336 | { rdf, graph: parsedAclGraph, webClient: mockWebClient }) 337 | let serializedGraph 338 | ps.serialize() 339 | .then(ttl => { 340 | serializedGraph = ttl 341 | return ps.save() 342 | }) 343 | .then(() => { 344 | t.ok(putStub.calledWith(aclUrl, serializedGraph, 'text/turtle'), 345 | 'ps.save() should result to a PUT to .acl url') 346 | t.end() 347 | }) 348 | .catch(err => { 349 | console.log(err) 350 | t.fail() 351 | }) 352 | }) 353 | 354 | test('PermissionSet save() no aclUrl test', t => { 355 | let nullAclUrl 356 | const ps = new PermissionSet(resourceUrl, nullAclUrl, false, 357 | { rdf, graph: parsedAclGraph }) 358 | ps.save() 359 | .then(() => { 360 | t.fail('ps.save() should not succeed with no acl url set') 361 | }) 362 | .catch(err => { 363 | t.equal(err.message, 'Cannot save - unknown target url') 364 | t.end() 365 | }) 366 | }) 367 | 368 | test('PermissionSet save() no web client test', t => { 369 | let nullAclUrl 370 | const ps = new PermissionSet(resourceUrl, aclUrl, false, 371 | { rdf, graph: parsedAclGraph }) 372 | ps.save() 373 | .then(() => { 374 | t.fail('ps.save() should not succeed with no web client set') 375 | }) 376 | .catch(err => { 377 | t.equal(err.message, 'Cannot save - no web client') 378 | t.end() 379 | }) 380 | }) 381 | --------------------------------------------------------------------------------