diff --git a/.travis.yml b/.travis.yml index e6e6386a0..f3ddcbba7 100644 --- a/.travis.yml +++ b/.travis.yml @@ -32,6 +32,8 @@ script: after_success: - snyk monitor +cache: npm + notifications: email: - solid@janeirodigital.com diff --git a/CHANGELOG.md b/CHANGELOG.md index 5d4f55728..d250c0fc2 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,15 +1,70 @@ # History +## 5.0.0 + +- Node versions greater than 8 are supported. +- Changes to vocabulary use: + - `solid:inbox` is deprecated in favour of `ldp:inbox`. + - `acl:defaultForNew` is deprecated in favour of `acl:default`. +- Terms of Service may be added and enforced for new registrations, + but is disabled by default. +- DELETE operations on a resource now require that the user has write permissions on + the file's container +- Improved support for logout ensures users can use different + identities. +- The profile container is now public readable by default. +- Access Control: + - The Access Control List system has undergone extensive + changes. Security has been tightened, and some unsafe practices that + where web apps was authorized access in the past are now not + permitted. + - The browser-reported `Origin` header will now be checked by + default, and the ACL system can be used to restrict access + to applications for added security. + - Users can add `trustedApp` entries to their profile using a new databrowser pane. + You will see an 'A' icon added while you view a Person's profile URL + with the data browser (might have to hit refresh in your browser and make sure you + are viewing a WebId URL like https://localhost:8443/profile/card#me). +- Logging is now verbose by default so the `-v` option has been + removed and a `--quiet` option has been added to mute the log. +- To be bug compliant with 4.x releases, if a rule for public readable + root / does not exist, it will check in /index.html.acl (see issue #1063) +- Command line options are now kebab-cased rather than camelCased, + config options may be both. +- Resource with no extension now have '$.ttl' appended in the filename (see upgrades notes below). +- Many smaller fixes. + +#### 5.0.0 Upgrade Notes + +- As of v5.0.0, all Turtle files need an extension. (**Intervention needed when updating from < 5.0.0!**) + - **How to upgrade?** + 1. Stop the server. + 2. Update node-solid-server to 5.0.0. + 3. Make a backup of your `data/` and `config/` folders. + 4. Invoke `solid migrate-legacy-resources -v`. + This makes the files in your `data/` and `config/` folders + automatically compatible with the new system. + You only need to do this once. + Different data folders can be migrated as well with the `-p` option: + `solid migrate-legacy-resources -p my/custom/data/folder -v` + 5. You can now start the server again as usual. + - **Why?** + Before version 5.0.0, `https://pod.example/profile/card` + would map to `file:///solid/profile/card`, with the _assumption_ + that it uses content-type `text/turtle`. + Now, this URL will map to `file:///solid/profile/card$.ttl` instead, + which makes the content-type automatically detectable. + This fixes many of the old Content-Type-related bugs. + _More information: https://www.w3.org/DesignIssues/HTTPFilenameMapping.html_ + ## 4.4.0 - Introduce a quota system. Delete the /settings/serverSide.ttl in the user's POD to disable, or edit to fit your resource constraints. - #### Changelog is incomplete for much of the 4.x series - -## 4.1.0 +## 4.1.0 - Add support for Group Access Control Lists. - Fix `Vary` header. diff --git a/README.md b/README.md index 6ae79fb72..bf38ed16e 100644 --- a/README.md +++ b/README.md @@ -167,7 +167,7 @@ $ solid start --help --root [value] Root folder to serve (default: './data') --port [value] SSL port to use - --serverUri [value] Solid server uri (default: 'https://localhost:8443') + --server-uri [value] Solid server uri (default: 'https://localhost:8443') --webid Enable WebID authentication and access control (uses HTTPS) --mount [value] Serve on a specific URL path (default: '/') --config-path [value] @@ -182,7 +182,7 @@ $ solid start --help --idp [value] Obsolete; use --multiuser --no-live Disable live support through WebSockets --proxy [value] Obsolete; use --corsProxy - --corsProxy [value] Serve the CORS proxy on this path + --cors-proxy [value] Serve the CORS proxy on this path --suppress-data-browser Suppress provision of a data browser --data-browser-path [value] An HTML file which is sent to allow users to browse the data (eg using mashlib.js) --suffix-acl [value] Suffix for acl files (default: '.acl') @@ -191,18 +191,21 @@ $ solid start --help --error-pages [value] Folder from which to look for custom error pages files (files must be named .html -- eg. 500.html) --force-user [value] Force a WebID to always be logged in (useful when offline) --strict-origin Enforce same origin policy in the ACL - --useEmail Do you want to set up an email service? + --use-email Do you want to set up an email service? --email-host [value] Host of your email service --email-port [value] Port of your email service --email-auth-user [value] User of your email service --email-auth-pass [value] Password of your email service - --useApiApps Do you want to load your default apps on /api/apps? + --use-api-apps Do you want to load your default apps on /api/apps? --api-apps [value] Path to the folder to mount on /api/apps --redirect-http-from [value] HTTP port or ','-separated ports to redirect to the solid server port (e.g. "80,8080"). --server-name [value] A name for your server (not required, but will be presented on your server's frontpage) --server-description [value] A description of your server (not required) --server-logo [value] A logo that represents you, your brand, or your server (not required) - -v, --verbose Print the logs to console + --enforce-toc Do you want to enforce Terms & Conditions for your service? + --toc-uri [value] URI to your Terms & Conditions + --support-email [value] The support email you provide for your users (not required) + -q, --quiet Do not print the logs to console -h, --help output usage information ``` @@ -226,8 +229,8 @@ docker run -p 8443:8443 --name solid node-solid-server ``` This will enable you to login to solid on https://localhost:8443 and then create a new account -but not yet use that account. After a new account is made you will need to create an entry for -it in your local (/etc/)hosts file in line with the account and subdomain i.e. +but not yet use that account. After a new account is made you will need to create an entry for +it in your local (/etc/)hosts file in line with the account and subdomain i.e. 127.0.0.1 newsoliduser.localhost @@ -391,14 +394,13 @@ blacklist profanities by default. ## Quota -By default, a file `serverSide.ttl` will be installed to new PODs. Its -current function is to set a quota for disk usage of just 25 MB, which -is what we can be sure the current prototype can tolerate under -load. This file is not writeable to users, but as server administrator -you can remove it if you don't want to impose a quota. It is currently -adviceable to remove it rather than set a large quota, because the -current implementation will impair write performance if there is a lot -of data. +By default, a file `serverSide.ttl.inactive` will be installed to new +PODs. If you rename it to `serverSide.ttl`, it will currently set a +quota for disk usage. This file is not writeable to users, only +server administrators who are authorized on the backend can modify +it. It is currently adviceable to remove it or set it inactive rather +than set a large quota, because the current implementation will impair +write performance if there is a lot of data. ## Contribute to Solid diff --git a/bin/lib/cli-utils.js b/bin/lib/cli-utils.js new file mode 100644 index 000000000..b578f6614 --- /dev/null +++ b/bin/lib/cli-utils.js @@ -0,0 +1,70 @@ +const fs = require('fs-extra') +const { cyan, bold } = require('colorette') +const { URL } = require('url') +const LDP = require('../../lib/ldp') +const AccountManager = require('../../lib/models/account-manager') +const SolidHost = require('../../lib/models/solid-host') + +module.exports.getAccountManager = getAccountManager +module.exports.loadAccounts = loadAccounts +module.exports.loadConfig = loadConfig +module.exports.loadUsernames = loadUsernames + +/** + * Returns an instance of AccountManager + * + * @param {Object} config + * @param {Object} [options] + * @returns {AccountManager} + */ +function getAccountManager (config, options = {}) { + const ldp = options.ldp || new LDP(config) + const host = options.host || SolidHost.from({ port: config.port, serverUri: config.serverUri }) + return AccountManager.from({ + host, + store: ldp, + multiuser: config.multiuser + }) +} + +function loadConfig (program, options) { + let argv = { + ...options, + version: program.version() + } + let configFile = argv['configFile'] || './config.json' + + try { + const file = fs.readFileSync(configFile) + + // Use flags with priority over config file + const config = JSON.parse(file) + argv = { ...config, ...argv } + } catch (err) { + // No file exists, not a problem + console.log(cyan(bold('TIP')), 'create a config.json: `$ solid init`') + } + + return argv +} + +/** + * + * @param root + * @param [serverUri] If not set, hostname must be set + * @param [hostname] If not set, serverUri must be set + * @returns {*} + */ +function loadAccounts ({ root, serverUri, hostname }) { + const files = fs.readdirSync(root) + hostname = hostname || new URL(serverUri).hostname + const isUserDirectory = new RegExp(`.${hostname}$`) + return files + .filter(file => isUserDirectory.test(file)) +} + +function loadUsernames ({ root, serverUri }) { + const hostname = new URL(serverUri).hostname + return loadAccounts({ root, hostname }) + .map(userDirectory => userDirectory.substr(0, userDirectory.length - hostname.length - 1)) +} diff --git a/bin/lib/cli.js b/bin/lib/cli.js index 8ac5e70c8..9131896d8 100644 --- a/bin/lib/cli.js +++ b/bin/lib/cli.js @@ -2,6 +2,8 @@ const program = require('commander') const loadInit = require('./init') const loadStart = require('./start') const loadInvalidUsernames = require('./invalidUsernames') +const loadMigrateLegacyResources = require('./migrateLegacyResources') +const loadUpdateIndex = require('./updateIndex') const { spawnSync } = require('child_process') const path = require('path') @@ -11,6 +13,8 @@ module.exports = function startCli (server) { loadInit(program) loadStart(program, server) loadInvalidUsernames(program) + loadMigrateLegacyResources(program) + loadUpdateIndex(program) program.parse(process.argv) if (program.args.length === 0) program.help() @@ -21,10 +25,15 @@ function getVersion () { // Obtain version from git const options = { cwd: __dirname, encoding: 'utf8' } const { stdout } = spawnSync('git', ['describe', '--tags'], options) - return stdout.trim() + const version = stdout.trim() + if (version === '') { + throw new Error('No git version here') + } + return version } catch (e) { // Obtain version from package.json const { version } = require(path.join(__dirname, '../../package.json')) return version } } + diff --git a/bin/lib/common.js b/bin/lib/common.js deleted file mode 100644 index 80a0f6e58..000000000 --- a/bin/lib/common.js +++ /dev/null @@ -1,25 +0,0 @@ -const fs = require('fs') -const extend = require('extend') -const { cyan, bold } = require('colorette') - -module.exports.loadConfig = loadConfig - -function loadConfig (program, options) { - let argv = extend({}, options, { version: program.version() }) - let configFile = argv['configFile'] || './config.json' - - try { - const file = fs.readFileSync(configFile) - - // Use flags with priority over config file - const config = JSON.parse(file) - Object.keys(config).forEach((option) => { - argv[option] = argv[option] || config[option] - }) - } catch (err) { - // No file exists, not a problem - console.log(cyan(bold('TIP')), 'create a config.json: `$ solid init`') - } - - return argv -} diff --git a/bin/lib/init.js b/bin/lib/init.js index 83a809e82..fa0deabb0 100644 --- a/bin/lib/init.js +++ b/bin/lib/init.js @@ -82,12 +82,12 @@ function manipulateEmailSection (answers) { function manipulateServerSection (answers) { answers.server = { - name: answers['server-name'], - description: answers['server-description'], - logo: answers['server-logo'] + name: answers['server-info-name'], + description: answers['server-info-description'], + logo: answers['server-info-logo'] } Object.keys(answers).forEach((answer) => { - if (answer.startsWith('server-')) { + if (answer.startsWith('server-info-')) { delete answers[answer] } }) diff --git a/bin/lib/invalidUsernames.js b/bin/lib/invalidUsernames.js index f2b7493e6..1d6cd340e 100644 --- a/bin/lib/invalidUsernames.js +++ b/bin/lib/invalidUsernames.js @@ -1,17 +1,14 @@ const fs = require('fs-extra') const Handlebars = require('handlebars') const path = require('path') -const { URL } = require('url') -const { loadConfig } = require('./common') +const { getAccountManager, loadConfig, loadUsernames } = require('./cli-utils') const { isValidUsername } = require('../../lib/common/user-utils') const blacklistService = require('../../lib/services/blacklist-service') const { initConfigDir, initTemplateDirs } = require('../../lib/server-config') const { fromServerConfig } = require('../../lib/models/oidc-manager') -const AccountManager = require('../../lib/models/account-manager') const EmailService = require('../../lib/services/email-service') -const LDP = require('../../lib/ldp') const SolidHost = require('../../lib/models/solid-host') module.exports = function (program) { @@ -28,7 +25,7 @@ module.exports = function (program) { const invalidUsernames = getInvalidUsernames(config) const host = SolidHost.from({ port: config.port, serverUri: config.serverUri }) - const accountManager = getAccountManager(config, host) + const accountManager = getAccountManager(config, { host }) if (options.notify) { return notifyUsers(invalidUsernames, accountManager, config) @@ -96,23 +93,9 @@ async function deleteUsers (usernames, accountManager, config, host) { console.info(`Deleted ${deletingUsers.length} users succeeded`) } -function getAccountManager (config, host) { - const ldp = new LDP(config) - return AccountManager.from({ - host, - store: ldp, - multiuser: config.multiuser - }) -} - function getInvalidUsernames (config) { - const files = fs.readdirSync(config.root) - const hostname = new URL(config.serverUri).hostname - const isUserDirectory = new RegExp(`.${hostname}$`) - return files - .filter(file => isUserDirectory.test(file)) - .map(userDirectory => userDirectory.substr(0, userDirectory.length - hostname.length - 1)) - .filter(username => !isValidUsername(username) || !blacklistService.validate(username)) + const usernames = loadUsernames(config) + return usernames.filter(username => !isValidUsername(username) || !blacklistService.validate(username)) } function listUsernames (usernames) { diff --git a/bin/lib/migrateLegacyResources.js b/bin/lib/migrateLegacyResources.js new file mode 100644 index 000000000..82895fb3c --- /dev/null +++ b/bin/lib/migrateLegacyResources.js @@ -0,0 +1,69 @@ +const fs = require('fs') +const Path = require('path') +const promisify = require('util').promisify +const readdir = promisify(fs.readdir) +const lstat = promisify(fs.lstat) +const rename = promisify(fs.rename) + +/* Converts the old (pre-5.0.0) extensionless files to $-based files _with_ extensions + * to make them work in the new resource mapper (post-5.0.0). + * By default, all extensionless files (that used to be interpreted as Turtle) will now receive a '$.ttl' suffix. */ +/* https://www.w3.org/DesignIssues/HTTPFilenameMapping.html */ + +module.exports = function (program) { + program + .command('migrate-legacy-resources') + .option('-p, --path ', 'Path to the data folder, defaults to \'data/\'') + .option('-s, --suffix ', 'The suffix to add to extensionless files, defaults to \'$.ttl\'') + .option('-v, --verbose', 'Path to the data folder') + .description('Migrate the data folder from node-solid-server 4 to node-solid-server 5') + .action(async (opts) => { + const verbose = opts.verbose + const suffix = opts.suffix || '$.ttl' + let paths = opts.path ? [ opts.path ] : [ 'data', 'config/templates' ] + paths = paths.map(path => path.startsWith(Path.sep) ? path : Path.join(process.cwd(), path)) + try { + for (const path of paths) { + if (verbose) { + console.log(`Migrating files in ${path}`) + } + await migrate(path, suffix, verbose) + } + } catch (err) { + console.error(err) + } + }) +} + +async function migrate (path, suffix, verbose) { + const files = await readdir(path) + for (const file of files) { + const fullFilePath = Path.join(path, file) + const stat = await lstat(fullFilePath) + if (stat.isFile()) { + if (shouldMigrateFile(file)) { + const newFullFilePath = getNewFileName(fullFilePath, suffix) + if (verbose) { + console.log(`${fullFilePath}\n => ${newFullFilePath}`) + } + await rename(fullFilePath, newFullFilePath) + } + } else { + if (shouldMigrateFolder(file)) { + await migrate(fullFilePath, suffix, verbose) + } + } + } +} + +function getNewFileName (fullFilePath, suffix) { + return fullFilePath + suffix +} + +function shouldMigrateFile (filename) { + return filename.indexOf('.') < 0 +} + +function shouldMigrateFolder (foldername) { + return foldername[0] !== '.' +} diff --git a/bin/lib/options.js b/bin/lib/options.js index f2585ce08..d5cd4fc85 100644 --- a/bin/lib/options.js +++ b/bin/lib/options.js @@ -26,7 +26,7 @@ module.exports = [ prompt: true }, { - name: 'serverUri', + name: 'server-uri', question: 'Solid server uri (with protocol, hostname and port)', help: "Solid server uri (default: 'https://localhost:8443')", default: 'https://localhost:8443', @@ -84,7 +84,7 @@ module.exports = [ } }, { - name: 'useOwner', + name: 'use-owner', question: 'Do you already have a WebID?', type: 'confirm', default: false, @@ -102,7 +102,7 @@ module.exports = [ return true }, when: function (answers) { - return answers.useOwner + return answers['use-owner'] } }, { @@ -148,7 +148,7 @@ module.exports = [ // help: 'URI to use as a default app for resources (default: https://linkeddata.github.io/warp/#/list/)' // }, { - name: 'useCorsProxy', + name: 'use-cors-proxy', help: 'Do you want to have a CORS proxy endpoint?', flag: true, default: false, @@ -160,16 +160,16 @@ module.exports = [ prompt: false }, { - name: 'corsProxy', + name: 'cors-proxy', help: 'Serve the CORS proxy on this path', when: function (answers) { - return answers.useCorsProxy + return answers['use-cors-proxy'] }, default: '/proxy', prompt: true }, { - name: 'authProxy', + name: 'auth-proxy', help: 'Object with path/server pairs to reverse proxy', default: {}, prompt: false, @@ -241,7 +241,7 @@ module.exports = [ prompt: false }, { - name: 'useEmail', + name: 'use-email', help: 'Do you want to set up an email service?', flag: true, prompt: true, @@ -253,7 +253,7 @@ module.exports = [ prompt: true, default: 'smtp.gmail.com', when: (answers) => { - return answers.useEmail + return answers['use-email'] } }, { @@ -262,7 +262,7 @@ module.exports = [ prompt: true, default: '465', when: (answers) => { - return answers.useEmail + return answers['use-email'] } }, { @@ -270,7 +270,7 @@ module.exports = [ help: 'User of your email service', prompt: true, when: (answers) => { - return answers.useEmail + return answers['use-email'] }, validate: (value) => { if (!value) { @@ -285,11 +285,11 @@ module.exports = [ type: 'password', prompt: true, when: (answers) => { - return answers.useEmail + return answers['use-email'] } }, { - name: 'useApiApps', + name: 'use-api-apps', help: 'Do you want to load your default apps on /api/apps?', flag: true, prompt: false, @@ -301,7 +301,7 @@ module.exports = [ prompt: true, validate: validPath, when: (answers) => { - return answers.useApiApps + return answers['use-api-apps'] } }, { // copied from name: 'owner' @@ -321,35 +321,46 @@ module.exports = [ } }, { - name: 'server-name', + // This property is packaged into an object for the server property in config.json + name: 'server-info-name', // All properties with prefix server-info- will be removed from the config help: 'A name for your server (not required, but will be presented on your server\'s frontpage)', prompt: true, - default: answers => new URL(answers.serverUri).hostname + default: answers => new URL(answers['server-uri']).hostname }, { - name: 'server-description', + // This property is packaged into an object for the server property in config.json + name: 'server-info-description', // All properties with prefix server-info- will be removed from the config help: 'A description of your server (not required)', prompt: true }, { - name: 'server-logo', + // This property is packaged into an object for the server property in config.json + name: 'server-info-logo', // All properties with prefix server-info- will be removed from the config help: 'A logo that represents you, your brand, or your server (not required)', prompt: true }, { - name: 'enforceToc', + name: 'enforce-toc', help: 'Do you want to enforce Terms & Conditions for your service?', flag: true, prompt: true, - default: true, + default: false, when: answers => answers.multiuser }, { - name: 'tocUri', + name: 'toc-uri', help: 'URI to your Terms & Conditions', prompt: true, validate: validUri, - when: answers => answers.enforceToc + when: answers => answers['enforce-toc'] + }, + { + name: 'disable-password-checks', + help: 'Do you want to disable password strength checking?', + flag: true, + prompt: true, + default: false, + when: answers => answers.multiuser }, { name: 'support-email', diff --git a/bin/lib/start.js b/bin/lib/start.js index ba8bf830a..635b7b4cd 100644 --- a/bin/lib/start.js +++ b/bin/lib/start.js @@ -2,7 +2,8 @@ const options = require('./options') const fs = require('fs') -const { loadConfig } = require('./common') +const path = require('path') +const { loadConfig } = require('./cli-utils') const { red, bold } = require('colorette') module.exports = function (program, server) { @@ -32,7 +33,7 @@ module.exports = function (program, server) { } }) - start.option('-v, --verbose', 'Print the logs to console') + start.option('-q, --quiet', 'Do not print the logs to console') start.action(async (options) => { const config = loadConfig(program, options) @@ -61,8 +62,8 @@ function bin (argv, server) { argv.live = !argv.noLive // Set up debug environment - if (argv.verbose) { - process.env.DEBUG = 'solid:*' + if (!argv.quiet) { + require('debug').enable('solid:*') } // Set up port @@ -84,10 +85,7 @@ function bin (argv, server) { // Overwrite root .acl if owner is specified if (argv.owner) { - let rootPath = argv.root - if (!rootPath) { - rootPath = process.cwd() - } + let rootPath = path.resolve(argv.root || process.cwd()) if (!(rootPath.endsWith('/'))) { rootPath += '/' } @@ -100,13 +98,13 @@ function bin (argv, server) { a n0:Authorization; n0:accessTo <./>; n0:agent <${argv.owner}>; - n0:defaultForNew <./>; + n0:default <./>; n0:mode n0:Control, n0:Read, n0:Write. <#everyone> a n0:Authorization; n0: n2:Agent; n0:accessTo <./>; - n0:defaultForNew <./>; + n0:default <./>; n0:mode n0:Read.` fs.writeFileSync(rootPath, defaultAcl) diff --git a/bin/lib/updateIndex.js b/bin/lib/updateIndex.js new file mode 100644 index 000000000..8412f7210 --- /dev/null +++ b/bin/lib/updateIndex.js @@ -0,0 +1,56 @@ +const fs = require('fs') +const path = require('path') +const cheerio = require('cheerio') +const LDP = require('../../lib/ldp') +const { URL } = require('url') +const debug = require('../../lib/debug') +const { readFile } = require('../../lib/common/fs-utils') + +const { compileTemplate, writeTemplate } = require('../../lib/common/template-utils') +const { loadConfig, loadAccounts } = require('./cli-utils') +const { getName, getWebId } = require('../../lib/common/user-utils') +const { initConfigDir, initTemplateDirs } = require('../../lib/server-config') + +module.exports = function (program) { + program + .command('updateindex') + .description('Update index.html in root of all PODs that haven\'t been marked otherwise') + .action(async (options) => { + const config = loadConfig(program, options) + const configPath = initConfigDir(config) + const templates = initTemplateDirs(configPath) + const indexTemplatePath = path.join(templates.account, 'index.html') + const indexTemplate = await compileTemplate(indexTemplatePath) + const ldp = new LDP(config) + const accounts = loadAccounts(config) + const usersProcessed = accounts.map(async account => { + const accountDirectory = path.join(config.root, account) + const indexFilePath = path.join(accountDirectory, '/index.html') + if (!isUpdateAllowed(indexFilePath)) { + return + } + const accountUrl = getAccountUrl(account, config) + try { + const webId = await getWebId(accountDirectory, accountUrl, ldp.suffixMeta, (filePath) => readFile(filePath)) + const name = await getName(webId, ldp.fetchGraph) + writeTemplate(indexFilePath, indexTemplate, { name, webId }) + } catch (err) { + debug.errors(`Failed to create new index for ${account}: ${JSON.stringify(err, null, 2)}`) + } + }) + await Promise.all(usersProcessed) + debug.accounts(`Processed ${usersProcessed.length} users`) + }) +} + +function getAccountUrl (name, config) { + const serverUrl = new URL(config.serverUri) + return `${serverUrl.protocol}//${name}.${serverUrl.host}/` +} + +function isUpdateAllowed (indexFilePath) { + const indexSource = fs.readFileSync(indexFilePath, 'utf-8') + const $ = cheerio.load(indexSource) + const allowAutomaticUpdateValue = $('meta[name="solid-allow-automatic-updates"]').prop('content') + return !allowAutomaticUpdateValue || allowAutomaticUpdateValue === 'true' +} diff --git a/bin/solid.js b/bin/solid.js deleted file mode 120000 index e72f8490f..000000000 --- a/bin/solid.js +++ /dev/null @@ -1 +0,0 @@ -solid \ No newline at end of file diff --git a/bin/solid.js b/bin/solid.js new file mode 100755 index 000000000..059baef66 --- /dev/null +++ b/bin/solid.js @@ -0,0 +1,3 @@ +#!/usr/bin/env node +const startCli = require('./lib/cli') +startCli() diff --git a/common/js/auth-buttons.js b/common/js/auth-buttons.js index 33004d9dc..49c28840e 100644 --- a/common/js/auth-buttons.js +++ b/common/js/auth-buttons.js @@ -3,7 +3,7 @@ (({ auth }) => { // Wire up DOM elements const [loginButton, logoutButton, registerButton, accountSettings] = - ['login', 'logout', 'register', 'accountSettings'].map(id => + ['login', 'logout', 'register', 'account-settings'].map(id => document.getElementById(id) || document.createElement('a')) loginButton.addEventListener('click', login) logoutButton.addEventListener('click', logout) @@ -15,6 +15,7 @@ const isOwner = loggedIn && new URL(session.webId).origin === location.origin loginButton.classList.toggle('hidden', loggedIn) logoutButton.classList.toggle('hidden', !loggedIn) + registerButton.classList.toggle('hidden', loggedIn) accountSettings.classList.toggle('hidden', !isOwner) }) diff --git a/common/js/solid.js b/common/js/solid.js index cdc604930..850b0610a 100644 --- a/common/js/solid.js +++ b/common/js/solid.js @@ -45,6 +45,8 @@ PasswordValidator.prototype.fetchDomNodes = function () { this.form = this.passwordField.closest('form') + this.disablePasswordChecks = this.passwordField.classList.contains('disable-password-checks') + this.passwordGroup = this.passwordField.closest('.form-group') this.passwordFeedback = this.passwordGroup.querySelector('.form-control-feedback') this.passwordStrengthMeter = this.passwordGroup.querySelector('.progress-bar') @@ -69,8 +71,10 @@ this.errors = [] this.resetValidation(this.passwordGroup) this.resetFeedbackIcon(this.passwordFeedback) - this.displayPasswordErrors() - this.instantFeedbackForPassword() + if (!this.disablePasswordChecks) { + this.displayPasswordErrors() + this.instantFeedbackForPassword() + } } /** @@ -99,14 +103,17 @@ PasswordValidator.prototype.validatePassword = function () { this.errors = [] const password = this.passwordField.value - const passwordStrength = this.getPasswordStrength(password) - this.currentStrengthLevel = this.getStrengthLevel(passwordStrength) - if (passwordStrength.errors) { - this.addPasswordError(passwordStrength.errors) - } + if (!this.disablePasswordChecks) { + const passwordStrength = this.getPasswordStrength(password) + this.currentStrengthLevel = this.getStrengthLevel(passwordStrength) + + if (passwordStrength.errors) { + this.addPasswordError(passwordStrength.errors) + } - this.checkLeakedPassword(password).then(this.handleLeakedPasswordResponse.bind(this)) + this.checkLeakedPassword(password).then(this.handleLeakedPasswordResponse.bind(this)) + } this.setPasswordFeedback() } diff --git a/config.json-default b/config.json-default index 8b1f95736..08e8d5475 100644 --- a/config.json-default +++ b/config.json-default @@ -16,6 +16,7 @@ "logo": "" }, "enforceToc": true, + "disablePasswordChecks": false, "tocUri": "https://your-toc", "supportEmail": "Your support email address" } diff --git a/config/defaults.js b/config/defaults.js index a2032929c..92b4ef70e 100644 --- a/config/defaults.js +++ b/config/defaults.js @@ -11,6 +11,8 @@ module.exports = { 'port': 8443, 'serverUri': 'https://localhost:8443', 'webid': true, + 'strictOrigin': true, + 'trustedOrigins': [], 'dataBrowserPath': 'default' // For use in Enterprises to configure a HTTP proxy for all outbound HTTP requests from the SOLID server (we use diff --git a/default-templates/new-account/.acl b/default-templates/new-account/.acl index 35258e69f..9f2213c84 100644 --- a/default-templates/new-account/.acl +++ b/default-templates/new-account/.acl @@ -1,23 +1,26 @@ # Root ACL resource for the user account @prefix acl: . +@prefix foaf: . -<#owner> +# The homepage is readable by the public +<#public> a acl:Authorization; + acl:agentClass foaf:Agent; + acl:accessTo ; + acl:mode acl:Read. - acl:agent <{{webId}}> ; - +# The owner has full access to every resource in their pod. +# Other agents have no access rights, +# unless specifically authorized in other .acl resources. +<#owner> + a acl:Authorization; + acl:agent <{{webId}}>; # Optional owner email, to be used for account recovery: {{#if email}}acl:agent ;{{/if}} - # Set the access to the root storage folder itself acl:accessTo ; - # All resources will inherit this authorization, by default - acl:defaultForNew ; - + acl:default ; # The owner has all of the access modes allowed acl:mode acl:Read, acl:Write, acl:Control. - -# Data is private by default; no other agents get access unless specifically -# authorized in other .acls diff --git a/default-templates/new-account/inbox/.acl b/default-templates/new-account/inbox/.acl index c10554678..17b8e4bb7 100644 --- a/default-templates/new-account/inbox/.acl +++ b/default-templates/new-account/inbox/.acl @@ -10,7 +10,7 @@ <{{webId}}>; acl:accessTo <./>; - acl:defaultForNew <./>; + acl:default <./>; acl:mode acl:Read, acl:Write, acl:Control. diff --git a/default-templates/new-account/index.html b/default-templates/new-account/index.html index 8364a4335..7fe96bfa7 100644 --- a/default-templates/new-account/index.html +++ b/default-templates/new-account/index.html @@ -3,6 +3,8 @@ + + {{#if name}}{{name}} – {{/if}}Solid Home diff --git a/default-templates/new-account/index.html.acl b/default-templates/new-account/index.html.acl deleted file mode 100644 index 47c7640a2..000000000 --- a/default-templates/new-account/index.html.acl +++ /dev/null @@ -1,22 +0,0 @@ -@prefix acl: . -@prefix foaf: . - -<#owner> - a acl:Authorization; - - acl:agent - <{{webId}}>; - - acl:accessTo ; - - acl:mode - acl:Read, acl:Write, acl:Control. - -<#public> - a acl:Authorization; - - acl:agentClass foaf:Agent; # everyone - - acl:accessTo <./index.html>; - - acl:mode acl:Read. diff --git a/default-templates/new-account/profile/.acl b/default-templates/new-account/profile/.acl new file mode 100644 index 000000000..a3cfaedc4 --- /dev/null +++ b/default-templates/new-account/profile/.acl @@ -0,0 +1,19 @@ +# ACL resource for the profile folder +@prefix acl: . +@prefix foaf: . + +# The owner has all permissions +<#owner> + a acl:Authorization; + acl:agent <{{webId}}>; + acl:accessTo <./>; + acl:defaultForNew <./>; + acl:mode acl:Read, acl:Write, acl:Control. + +# The public has read permissions +<#public> + a acl:Authorization; + acl:agentClass foaf:Agent; + acl:accessTo <./>; + acl:defaultForNew <./>; + acl:mode acl:Read. diff --git a/default-templates/new-account/profile/card b/default-templates/new-account/profile/card$.ttl similarity index 100% rename from default-templates/new-account/profile/card rename to default-templates/new-account/profile/card$.ttl diff --git a/default-templates/new-account/profile/card.acl b/default-templates/new-account/profile/card.acl deleted file mode 100644 index 335aa13da..000000000 --- a/default-templates/new-account/profile/card.acl +++ /dev/null @@ -1,25 +0,0 @@ -# ACL for the WebID Profile document - -@prefix acl: . -@prefix foaf: . - -<#owner> - a acl:Authorization; - - acl:agent - <{{webId}}>; - - acl:accessTo <./card>; - - acl:mode - acl:Read, acl:Write, acl:Control. - -# Public-readable -<#public> - a acl:Authorization; - - acl:agentClass foaf:Agent; # everyone - - acl:accessTo <./card>; - - acl:mode acl:Read. diff --git a/default-templates/new-account/public/.acl b/default-templates/new-account/public/.acl index c289aedfe..210555a83 100644 --- a/default-templates/new-account/public/.acl +++ b/default-templates/new-account/public/.acl @@ -7,7 +7,7 @@ a acl:Authorization; acl:agent <{{webId}}>; acl:accessTo <./>; - acl:defaultForNew <./>; + acl:default <./>; acl:mode acl:Read, acl:Write, acl:Control. # The public has read permissions @@ -15,5 +15,5 @@ a acl:Authorization; acl:agentClass foaf:Agent; acl:accessTo <./>; - acl:defaultForNew <./>; + acl:default <./>; acl:mode acl:Read. diff --git a/default-templates/new-account/settings/.acl b/default-templates/new-account/settings/.acl index bc871390f..921e65570 100644 --- a/default-templates/new-account/settings/.acl +++ b/default-templates/new-account/settings/.acl @@ -11,7 +11,7 @@ acl:accessTo <./>; # All settings resources will be private, by default, unless overridden - acl:defaultForNew <./>; + acl:default <./>; # The owner has all of the access modes allowed acl:mode diff --git a/default-templates/new-account/settings/privateTypeIndex.ttl b/default-templates/new-account/settings/privateTypeIndex.ttl index 607f6321d..b6fee77e6 100644 --- a/default-templates/new-account/settings/privateTypeIndex.ttl +++ b/default-templates/new-account/settings/privateTypeIndex.ttl @@ -1,4 +1,4 @@ -@prefix solid: . +@prefix solid: . <> a solid:TypeIndex ; a solid:UnlistedDocument. diff --git a/default-templates/new-account/settings/publicTypeIndex.ttl b/default-templates/new-account/settings/publicTypeIndex.ttl index e270db393..433486252 100644 --- a/default-templates/new-account/settings/publicTypeIndex.ttl +++ b/default-templates/new-account/settings/publicTypeIndex.ttl @@ -1,4 +1,4 @@ -@prefix solid: . +@prefix solid: . <> a solid:TypeIndex ; a solid:ListedDocument. diff --git a/default-templates/new-account/settings/serverSide.ttl b/default-templates/new-account/settings/serverSide.ttl.inactive similarity index 100% rename from default-templates/new-account/settings/serverSide.ttl rename to default-templates/new-account/settings/serverSide.ttl.inactive diff --git a/default-templates/server/.acl b/default-templates/server/.acl new file mode 100644 index 000000000..05a9842d9 --- /dev/null +++ b/default-templates/server/.acl @@ -0,0 +1,10 @@ +# Root ACL resource for the root +@prefix acl: . +@prefix foaf: . + +<#public> + a acl:Authorization; + acl:agentClass foaf:Agent; # everyone + acl:accessTo ; + acl:default ; + acl:mode acl:Read. diff --git a/default-views/account/register-form.hbs b/default-views/account/register-form.hbs index 53714f2f0..63f51da04 100644 --- a/default-views/account/register-form.hbs +++ b/default-views/account/register-form.hbs @@ -36,7 +36,7 @@
- +
diff --git a/default-views/auth/login-required.hbs b/default-views/auth/login-required.hbs index 80a1344cc..467a3a655 100644 --- a/default-views/auth/login-required.hbs +++ b/default-views/auth/login-required.hbs @@ -13,6 +13,7 @@
+

Log in to access this resource

diff --git a/lib/acl-checker.js b/lib/acl-checker.js index f60527324..be3260582 100644 --- a/lib/acl-checker.js +++ b/lib/acl-checker.js @@ -1,77 +1,138 @@ 'use strict' -const PermissionSet = require('solid-permissions').PermissionSet const rdf = require('rdflib') const debug = require('./debug').ACL const HTTPError = require('./http-error') +const aclCheck = require('@solid/acl-check') +const { URL } = require('url') +const { promisify } = require('util') +const fs = require('fs') const DEFAULT_ACL_SUFFIX = '.acl' +const ACL = rdf.Namespace('http://www.w3.org/ns/auth/acl#') // An ACLChecker exposes the permissions on a specific resource class ACLChecker { constructor (resource, options = {}) { this.resource = resource - this.host = options.host - this.origin = options.origin + this.resourceUrl = new URL(resource) + this.agentOrigin = options.strictOrigin && options.agentOrigin ? rdf.sym(options.agentOrigin) : null this.fetch = options.fetch this.fetchGraph = options.fetchGraph - this.strictOrigin = options.strictOrigin + this.trustedOrigins = options.strictOrigin && options.trustedOrigins ? options.trustedOrigins.map(trustedOrigin => rdf.sym(trustedOrigin)) : null this.suffix = options.suffix || DEFAULT_ACL_SUFFIX + this.aclCached = {} + this.messagesCached = {} + this.requests = {} } // Returns a fulfilled promise when the user can access the resource // in the given mode, or rejects with an HTTP error otherwise - can (user, mode) { + async can (user, mode) { + const cacheKey = `${mode}-${user}` + if (this.aclCached[cacheKey]) { + return this.aclCached[cacheKey] + } + this.messagesCached[cacheKey] = this.messagesCached[cacheKey] || [] + + const acl = await this.getNearestACL().catch(err => { + this.messagesCached[cacheKey].push(new HTTPError(err.status || 500, err.message || err)) + }) + if (!acl) { + this.aclCached[cacheKey] = Promise.resolve(false) + return this.aclCached[cacheKey] + } + let resource = rdf.sym(this.resource) + if (this.resource.endsWith('/' + this.suffix)) { + resource = rdf.sym(ACLChecker.getDirectory(this.resource)) + } // If this is an ACL, Control mode must be present for any operations if (this.isAcl(this.resource)) { mode = 'Control' + resource = rdf.sym(this.resource.substring(0, this.resource.length - this.suffix.length)) } - - // Obtain the permission set for the resource - if (!this._permissionSet) { - this._permissionSet = this.getNearestACL() - .then(acl => this.getPermissionSet(acl)) + const directory = acl.isContainer ? rdf.sym(ACLChecker.getDirectory(acl.acl)) : null + const aclFile = rdf.sym(acl.acl) + const agent = user ? rdf.sym(user) : null + const modes = [ACL(mode)] + const agentOrigin = this.agentOrigin + const trustedOrigins = this.trustedOrigins + let originTrustedModes = [] + try { + this.fetch(aclFile.doc().value) + originTrustedModes = await aclCheck.getTrustedModesForOrigin(acl.graph, resource, directory, aclFile, agentOrigin, (uriNode) => { + return this.fetch(uriNode.doc().value, acl.graph) + }) + } catch (e) { + // FIXME: https://github.com/solid/acl-check/issues/23 + // console.error(e.message) + } + const accessDenied = aclCheck.accessDenied(acl.graph, resource, directory, aclFile, agent, modes, agentOrigin, trustedOrigins, originTrustedModes) + if (accessDenied && user) { + this.messagesCached[cacheKey].push(HTTPError(403, accessDenied)) + } else if (accessDenied) { + this.messagesCached[cacheKey].push(HTTPError(401, 'Unauthenticated')) } + this.aclCached[cacheKey] = Promise.resolve(!accessDenied) + return this.aclCached[cacheKey] + } - // Check the resource's permissions - return this._permissionSet - .then(acls => this.checkAccess(acls, user, mode)) - .catch(() => { - if (!user) { - throw new HTTPError(401, `Access to ${this.resource} requires authorization`) - } else { - throw new HTTPError(403, `Access to ${this.resource} denied for ${user}`) - } - }) + async getError (user, mode) { + const cacheKey = `${mode}-${user}` + this.aclCached[cacheKey] = this.aclCached[cacheKey] || this.can(user, mode) + const isAllowed = await this.aclCached[cacheKey] + return isAllowed ? null : this.messagesCached[cacheKey].reduce((prevMsg, msg) => msg.status > prevMsg.status ? msg : prevMsg, { status: 0 }) + } + + static getDirectory (aclFile) { + const parts = aclFile.split('/') + parts.pop() + return `${parts.join('/')}/` } // Gets the ACL that applies to the resource - getNearestACL () { + async getNearestACL () { const { resource } = this let isContainer = false - // Create a cascade of reject handlers (one for each possible ACL) - const nearestACL = this.getPossibleACLs().reduce((prevACL, acl) => { - return prevACL.catch(() => new Promise((resolve, reject) => { - this.fetch(acl, (err, graph) => { - if (err || !graph || !graph.length) { - isContainer = true - reject(err) - } else { - const relative = resource.replace(acl.replace(/[^/]+$/, ''), './') - debug(`Using ACL ${acl} for ${relative}`) - resolve({ acl, graph, isContainer }) - } - }) - })) - }, Promise.reject()) - return nearestACL.catch(e => { throw new Error('No ACL resource found') }) + const possibleACLs = this.getPossibleACLs() + const acls = [...possibleACLs] + let returnAcl = null + while (possibleACLs.length > 0 && !returnAcl) { + const acl = possibleACLs.shift() + let graph + try { + this.requests[acl] = this.requests[acl] || this.fetch(acl) + graph = await this.requests[acl] + } catch (err) { + if (err && (err.code === 'ENOENT' || err.status === 404)) { + isContainer = true + continue + } + debug(err) + throw err + } + const relative = resource.replace(acl.replace(/[^/]+$/, ''), './') + debug(`Using ACL ${acl} for ${relative}`) + returnAcl = { acl, graph, isContainer } + } + if (!returnAcl) { + throw new HTTPError(500, `No ACL found for ${resource}, searched in \n- ${acls.join('\n- ')}`) + } + const groupNodes = returnAcl.graph.statementsMatching(null, ACL('agentGroup'), null) + const groupUrls = groupNodes.map(node => node.object.value.split('#')[0]) + await Promise.all(groupUrls.map(groupUrl => { + this.requests[groupUrl] = this.requests[groupUrl] || this.fetch(groupUrl, returnAcl.graph) + return this.requests[groupUrl] + })) + + return returnAcl } // Gets all possible ACL paths that apply to the resource getPossibleACLs () { // Obtain the resource URI and the length of its base let { resource: uri, suffix } = this - const [ { length: base } ] = uri.match(/^[^:]+:\/*[^/]+/) + const [{ length: base }] = uri.match(/^[^:]+:\/*[^/]+/) // If the URI points to a file, append the file's ACL const possibleAcls = [] @@ -86,44 +147,51 @@ class ACLChecker { return possibleAcls } - // Tests whether the permissions allow a given operation - checkAccess (permissionSet, user, mode) { - const options = { fetchGraph: this.fetchGraph } - return permissionSet.checkAccess(this.resource, user, mode, options) - .then(hasAccess => { - if (hasAccess) { - return true - } else { - throw new Error('ACL file found but no matching policy found') - } - }) - } - - // Gets the permission set for the given ACL - getPermissionSet ({ acl, graph, isContainer }) { - if (!graph || graph.length === 0) { - debug('ACL ' + acl + ' is empty') - throw new Error('No policy found - empty ACL') - } - const aclOptions = { - aclSuffix: this.suffix, - graph: graph, - host: this.host, - origin: this.origin, - rdf: rdf, - strictOrigin: this.strictOrigin, - isAcl: uri => this.isAcl(uri), - aclUrlFor: uri => this.aclUrlFor(uri) - } - return new PermissionSet(this.resource, acl, isContainer, aclOptions) + isAcl (resource) { + return resource.endsWith(this.suffix) } - aclUrlFor (uri) { - return this.isAcl(uri) ? uri : uri + this.suffix + static createFromLDPAndRequest (resource, ldp, req) { + const trustedOrigins = ldp.getTrustedOrigins(req) + return new ACLChecker(resource, { + agentOrigin: req.get('origin'), + // host: req.get('host'), + fetch: fetchFromLdp(ldp.resourceMapper), + fetchGraph: (uri, options) => { + // first try loading from local fs + return ldp.getGraph(uri, options.contentType) + // failing that, fetch remote graph + .catch(() => ldp.fetchGraph(uri, options)) + }, + suffix: ldp.suffixAcl, + strictOrigin: ldp.strictOrigin, + trustedOrigins + }) } +} - isAcl (resource) { - return resource.endsWith(this.suffix) +/** + * Returns a fetch document handler used by the ACLChecker to fetch .acl + * resources up the inheritance chain. + * The `fetch(uri, callback)` results in the callback, with either: + * - `callback(err, graph)` if any error is encountered, or + * - `callback(null, graph)` with the parsed RDF graph of the fetched resource + * @return {Function} Returns a `fetch(uri, callback)` handler + */ +function fetchFromLdp (mapper) { + return async function fetch (url, graph = rdf.graph()) { + // Convert the URL into a filename + let path, contentType + try { + ({ path, contentType } = await mapper.mapUrlToFile({ url })) + } catch (err) { + throw new HTTPError(404, err) + } + // Read the file from disk + const body = await promisify(fs.readFile)(path, { 'encoding': 'utf8' }) + // Parse the file as Turtle + rdf.parse(body, graph, url, contentType) + return graph } } diff --git a/lib/api/accounts/user-accounts.js b/lib/api/accounts/user-accounts.js index cc5daafd0..2d58631cd 100644 --- a/lib/api/accounts/user-accounts.js +++ b/lib/api/accounts/user-accounts.js @@ -4,6 +4,8 @@ const express = require('express') const bodyParser = require('body-parser').urlencoded({ extended: false }) const debug = require('../../debug').accounts +const restrictToTopDomain = require('../../handlers/restrict-to-top-domain') + const CreateAccountRequest = require('../../requests/create-account-request') const AddCertificateRequest = require('../../requests/add-cert-request') const DeleteAccountRequest = require('../../requests/delete-account-request') @@ -65,16 +67,16 @@ function middleware (accountManager) { router.get('/', checkAccountExists(accountManager)) - router.post('/api/accounts/new', bodyParser, CreateAccountRequest.post) - router.get(['/register', '/api/accounts/new'], CreateAccountRequest.get) + router.post('/api/accounts/new', restrictToTopDomain, bodyParser, CreateAccountRequest.post) + router.get(['/register', '/api/accounts/new'], restrictToTopDomain, CreateAccountRequest.get) - router.post('/api/accounts/cert', bodyParser, newCertificate(accountManager)) + router.post('/api/accounts/cert', restrictToTopDomain, bodyParser, newCertificate(accountManager)) - router.get('/account/delete', DeleteAccountRequest.get) - router.post('/account/delete', bodyParser, DeleteAccountRequest.post) + router.get('/account/delete', restrictToTopDomain, DeleteAccountRequest.get) + router.post('/account/delete', restrictToTopDomain, bodyParser, DeleteAccountRequest.post) - router.get('/account/delete/confirm', DeleteAccountConfirmRequest.get) - router.post('/account/delete/confirm', bodyParser, DeleteAccountConfirmRequest.post) + router.get('/account/delete/confirm', restrictToTopDomain, DeleteAccountConfirmRequest.get) + router.post('/account/delete/confirm', restrictToTopDomain, bodyParser, DeleteAccountConfirmRequest.post) return router } diff --git a/lib/api/authn/webid-oidc.js b/lib/api/authn/webid-oidc.js index 9e2fc5cb7..798bd6d47 100644 --- a/lib/api/authn/webid-oidc.js +++ b/lib/api/authn/webid-oidc.js @@ -9,13 +9,12 @@ const bodyParser = require('body-parser').urlencoded({ extended: false }) const OidcManager = require('../../models/oidc-manager') const { LoginRequest } = require('../../requests/login-request') +const restrictToTopDomain = require('../../handlers/restrict-to-top-domain') + const PasswordResetEmailRequest = require('../../requests/password-reset-email-request') const PasswordChangeRequest = require('../../requests/password-change-request') -const { - AuthCallbackRequest, - LogoutRequest -} = require('@solid/oidc-auth-manager').handlers +const { AuthCallbackRequest } = require('@solid/oidc-auth-manager').handlers /** * Sets up OIDC authentication for the given app. @@ -74,14 +73,12 @@ function middleware (oidc) { router.post('/login/tls', bodyParser, LoginRequest.loginTls) - router.get('/account/password/reset', PasswordResetEmailRequest.get) - router.post('/account/password/reset', bodyParser, PasswordResetEmailRequest.post) + router.get('/account/password/reset', restrictToTopDomain, PasswordResetEmailRequest.get) + router.post('/account/password/reset', restrictToTopDomain, bodyParser, PasswordResetEmailRequest.post) - router.get('/account/password/change', PasswordChangeRequest.get) - router.post('/account/password/change', bodyParser, PasswordChangeRequest.post) + router.get('/account/password/change', restrictToTopDomain, PasswordChangeRequest.get) + router.post('/account/password/change', restrictToTopDomain, bodyParser, PasswordChangeRequest.post) - router.get('/logout', LogoutRequest.handle) - router.post('/logout', LogoutRequest.handle) router.get('/.well-known/solid/logout/', (req, res) => res.redirect('/logout')) router.get('/goodbye', (req, res) => { res.render('auth/goodbye') }) @@ -92,9 +89,7 @@ function middleware (oidc) { // Static assets related to authentication const authAssets = [ ['/.well-known/solid/login/', '../static/popup-redirect.html', false], - ['/common/', 'solid-auth-client/dist-popup/popup.html'], - ['/common/js/', 'solid-auth-client/dist-lib/solid-auth-client.bundle.js'], - ['/common/js/', 'solid-auth-client/dist-lib/solid-auth-client.bundle.js.map'] + ['/common/', 'solid-auth-client/dist-popup/popup.html'] ] authAssets.map(args => routeResolvedFile(router, ...args)) diff --git a/lib/capability-discovery.js b/lib/capability-discovery.js index 314ae8a3f..f96fe021d 100644 --- a/lib/capability-discovery.js +++ b/lib/capability-discovery.js @@ -3,23 +3,7 @@ * @module capability-discovery */ const express = require('express') -const util = require('./utils') - -const serviceConfigDefaults = { - 'api': { - 'accounts': { - // 'changePassword': '/api/account/changePassword', - // 'delete': '/api/accounts/delete', - - // Create new user (see IdentityProvider.post() in identity-provider.js) - 'new': '/api/accounts/new', - 'recover': '/api/accounts/recover', - 'signin': '/login', - 'signout': '/logout', - 'validateToken': '/api/accounts/validateToken' - } - } -} +const { URL } = require('url') module.exports = capabilityDiscovery @@ -32,7 +16,7 @@ function capabilityDiscovery () { const router = express.Router('/') // Advertise the server capability discover endpoint - router.get('/.well-known/solid', serviceCapabilityDocument(serviceConfigDefaults)) + router.get('/.well-known/solid', serviceCapabilityDocument()) return router } @@ -44,12 +28,27 @@ function capabilityDiscovery () { * @param res * @param next */ -function serviceCapabilityDocument (serviceConfig) { +function serviceCapabilityDocument () { return (req, res) => { - // Add the server root url - serviceConfig.root = util.getFullUri(req) // TODO make sure we align with the rest - // Add the 'apps' urls section - serviceConfig.apps = req.app.locals.appUrls - res.json(serviceConfig) + const ldp = req.app.locals.ldp + res.json({ + // Add the server root url + root: req.app.locals.ldp.resourceMapper.resolveUrl(req.hostname, req.path), + // Add the 'apps' urls section + apps: req.app.locals.appUrls, + api: { + accounts: { + // 'changePassword': '/api/account/changePassword', + // 'delete': '/api/accounts/delete', + + // Create new user (see IdentityProvider.post() in identity-provider.js) + new: new URL('/api/accounts/new', ldp.serverUri), + recover: new URL('/api/accounts/recover', ldp.serverUri), + signin: req.app.locals.ldp.resourceMapper.resolveUrl(req.hostname, '/login'), + signout: req.app.locals.ldp.resourceMapper.resolveUrl(req.hostname, '/logout'), + validateToken: new URL('/api/accounts/validateToken', ldp.serverUri) + } + } + }) } } diff --git a/lib/common/fs-utils.js b/lib/common/fs-utils.js index 083a2bdc0..8da615064 100644 --- a/lib/common/fs-utils.js +++ b/lib/common/fs-utils.js @@ -1,5 +1,7 @@ module.exports.copyTemplateDir = copyTemplateDir module.exports.processFile = processFile +module.exports.readFile = readFile +module.exports.writeFile = writeFile const fs = require('fs-extra') @@ -31,3 +33,11 @@ async function processFile (filePath, manipulateSourceFn) { }) }) } + +function readFile (filePath, options = 'utf-8') { + return fs.readFileSync(filePath, options) +} + +function writeFile (filePath, fileSource, options = 'utf-8') { + fs.writeFileSync(filePath, fileSource, options) +} diff --git a/lib/common/template-utils.js b/lib/common/template-utils.js index 78bbb1479..afb6f05e8 100644 --- a/lib/common/template-utils.js +++ b/lib/common/template-utils.js @@ -1,8 +1,15 @@ +module.exports.compileTemplate = compileTemplate module.exports.processHandlebarFile = processHandlebarFile +module.exports.writeTemplate = writeTemplate const Handlebars = require('handlebars') const debug = require('../debug').errors -const { processFile } = require('./fs-utils') +const { processFile, readFile, writeFile } = require('./fs-utils') + +async function compileTemplate (filePath) { + const indexTemplateSource = readFile(filePath) + return Handlebars.compile(indexTemplateSource) +} /** * Reads a file, processes it (performing template substitution), and saves @@ -36,3 +43,9 @@ function processHandlebarTemplate (source, substitutions) { return source } } + +function writeTemplate (filePath, template, substitutions) { + const source = template(substitutions) + writeFile(filePath, source) +} + diff --git a/lib/common/user-utils.js b/lib/common/user-utils.js index 3c0c46a69..c5bcd00b8 100644 --- a/lib/common/user-utils.js +++ b/lib/common/user-utils.js @@ -1,5 +1,28 @@ +const $rdf = require('rdflib') + +const SOLID = $rdf.Namespace('http://www.w3.org/ns/solid/terms#') +const VCARD = $rdf.Namespace('http://www.w3.org/2006/vcard/ns#') + +module.exports.getName = getName +module.exports.getWebId = getWebId module.exports.isValidUsername = isValidUsername +async function getName (webId, fetchGraph) { + const graph = await fetchGraph(webId) + const nameNode = graph.any($rdf.sym(webId), VCARD('fn')) + return nameNode.value +} + +async function getWebId (accountDirectory, accountUrl, suffixMeta, fetchData) { + const metaFilePath = `${accountDirectory}/${suffixMeta}` + const metaFileUri = `${accountUrl}${suffixMeta}` + const metaData = await fetchData(metaFilePath) + const metaGraph = $rdf.graph() + $rdf.parse(metaData, metaGraph, metaFileUri, 'text/turtle') + const webIdNode = metaGraph.any(undefined, SOLID('account'), $rdf.sym(accountUrl)) + return webIdNode.value +} + function isValidUsername (username) { return /^[a-z0-9]+(?:-[a-z0-9]+)*$/.test(username) } diff --git a/lib/create-app.js b/lib/create-app.js index db448d20b..9b4be11e8 100644 --- a/lib/create-app.js +++ b/lib/create-app.js @@ -20,9 +20,12 @@ const errorPages = require('./handlers/error-pages') const config = require('./server-config') const defaults = require('../config/defaults') const options = require('./handlers/options') -const debug = require('./debug').authentication +const debug = require('./debug') const path = require('path') const { routeResolvedFile } = require('./utils') +const ResourceMapper = require('./resource-mapper') +const aclCheck = require('@solid/acl-check') +const { version } = require('../package.json') const corsSettings = cors({ methods: [ @@ -39,11 +42,24 @@ function createApp (argv = {}) { // Override default configs (defaults) with passed-in params (argv) argv = Object.assign({}, defaults, argv) - argv.host = SolidHost.from({ port: argv.port, serverUri: argv.serverUri }) + argv.host = SolidHost.from(argv) + + argv.resourceMapper = new ResourceMapper({ + rootUrl: argv.serverUri, + rootPath: path.resolve(argv.root || process.cwd()), + includeHost: argv.multiuser, + defaultContentType: argv.defaultContentType, + fileSuffixes: [ + argv.suffixAcl || '.acl', + argv.suffixMeta || '.meta' + ] + }) const configPath = config.initConfigDir(argv) argv.templates = config.initTemplateDirs(configPath) + config.printDebugInfo(argv) + const ldp = new LDP(argv) const app = express() @@ -51,11 +67,15 @@ function createApp (argv = {}) { initAppLocals(app, argv, ldp) initHeaders(app) initViews(app, configPath) + initLoggers() // Serve the public 'common' directory (for shared CSS files, etc) app.use('/common', express.static(path.join(__dirname, '../common'))) + routeResolvedFile(app, '/common/js/', 'mashlib/dist/mashlib.js') routeResolvedFile(app, '/common/js/', 'mashlib/dist/mashlib.min.js') routeResolvedFile(app, '/common/js/', 'mashlib/dist/mashlib.min.js.map') + routeResolvedFile(app, '/common/js/', 'solid-auth-client/dist-lib/solid-auth-client.bundle.js') + routeResolvedFile(app, '/common/js/', 'solid-auth-client/dist-lib/solid-auth-client.bundle.js.map') app.use('/.well-known', express.static(path.join(__dirname, '../common/well-known'))) // Serve bootstrap from it's node_module directory @@ -125,6 +145,7 @@ function initAppLocals (app, argv, ldp) { app.locals.tokenService = new TokenService() app.locals.enforceToc = argv.enforceToc app.locals.tocUri = argv.tocUri + app.locals.disablePasswordChecks = argv.disablePasswordChecks if (argv.email && argv.email.host) { app.locals.emailService = new EmailService(argv.templates.email, argv.email) @@ -140,7 +161,7 @@ function initHeaders (app) { app.use(corsSettings) app.use((req, res, next) => { - res.set('X-Powered-By', 'solid-server') + res.set('X-Powered-By', 'solid-server/' + version) // Cors lib adds Vary: Origin automatically, but inreliably res.set('Vary', 'Accept, Authorization, Origin') @@ -190,15 +211,29 @@ function initWebId (argv, app, ldp) { // any third-party application could perform authenticated requests // without permission by including the credentials set by the Solid server. app.use((req, res, next) => { - const origin = req.headers.origin + const origin = req.get('origin') + const trustedOrigins = ldp.getTrustedOrigins(req) const userId = req.session.userId // Exception: allow logout requests from all third-party apps // such that OIDC client can log out via cookie auth // TODO: remove this exception when OIDC clients // use Bearer token to authenticate instead of cookie // (https://github.com/solid/node-solid-server/pull/835#issuecomment-426429003) - if (!argv.host.allowsSessionFor(userId, origin) && !isLogoutRequest(req)) { - debug(`Rejecting session for ${userId} from ${origin}`) + // + // Authentication cookies are an optimization: + // instead of going through the process of + // fully validating authentication on every request, + // we go through this process once, + // and store its successful result in a cookie + // that will be reused upon the next request. + // However, that cookie can then be sent by any server, + // even servers that have not gone through the proper authentication mechanism. + // However, if trusted origins are enabled, + // then any origin is allowed to take the shortcut route, + // since malicious origins will be banned at the ACL checking phase. + // https://github.com/solid/node-solid-server/issues/1117 + if (!argv.strictOrigin && !argv.host.allowsSessionFor(userId, origin, trustedOrigins) && !isLogoutRequest(req)) { + debug.authentication(`Rejecting session for ${userId} from ${origin}`) // Destroy session data delete req.session.userId // Ensure this modified session is not saved @@ -229,6 +264,10 @@ function initWebId (argv, app, ldp) { } } +function initLoggers () { + aclCheck.configureLogger(debug.ACL) +} + /** * Determines whether the given request is a logout request */ diff --git a/lib/handlers/allow.js b/lib/handlers/allow.js index f4c2e5f48..851b93fa7 100644 --- a/lib/handlers/allow.js +++ b/lib/handlers/allow.js @@ -1,89 +1,73 @@ module.exports = allow +const path = require('path') const ACL = require('../acl-checker') -const $rdf = require('rdflib') -const utils = require('../utils') const debug = require('../debug.js').ACL -const LegacyResourceMapper = require('../legacy-resource-mapper') -function allow (mode) { - return function allowHandler (req, res, next) { +function allow (mode, checkPermissionsForDirectory) { + return async function allowHandler (req, res, next) { const ldp = req.app.locals.ldp || {} if (!ldp.webid) { return next() } // Set up URL to filesystem mapping - const rootUrl = utils.getBaseUri(req) - const mapper = new LegacyResourceMapper({ - rootUrl, - rootPath: ldp.root, - includeHost: ldp.multiuser - }) + const rootUrl = ldp.resourceMapper.resolveUrl(req.hostname) // Determine the actual path of the request // (This is used as an ugly hack to check the ACL status of other resources.) - let reqPath = res && res.locals && res.locals.path + let resourcePath = res && res.locals && res.locals.path ? res.locals.path : req.path + // Check permissions of the directory instead of the file itself. + if (checkPermissionsForDirectory) { + resourcePath = path.dirname(resourcePath) + } + // Check whether the resource exists - ldp.exists(req.hostname, reqPath, (err, ret) => { - // Ensure directories always end in a slash - const stat = err ? null : ret.stream - if (!reqPath.endsWith('/') && stat && stat.isDirectory()) { - reqPath += '/' - } + let stat + try { + const ret = await ldp.exists(req.hostname, resourcePath) + stat = ret.stream + } catch (err) { + stat = null + } - // Obtain and store the ACL of the requested resource - req.acl = new ACL(rootUrl + reqPath, { - origin: req.get('origin'), - host: req.protocol + '://' + req.get('host'), - fetch: fetchFromLdp(mapper, ldp), - fetchGraph: (uri, options) => { - // first try loading from local fs - return ldp.getGraph(uri, options.contentType) - // failing that, fetch remote graph - .catch(() => ldp.fetchGraph(uri, options)) - }, - suffix: ldp.suffixAcl, - strictOrigin: ldp.strictOrigin - }) + // Ensure directories always end in a slash + if (!resourcePath.endsWith('/') && stat && stat.isDirectory()) { + resourcePath += '/' + } - // Ensure the user has the required permission - const userId = req.session.userId - req.acl.can(userId, mode) - .then(() => next(), err => { - debug(`${mode} access denied to ${userId || '(none)'}`) - next(err) - }) - }) - } -} + let trustedOrigins = [ldp.resourceMapper.resolveUrl(req.hostname)].concat(ldp.trustedOrigins) + if (ldp.multiuser) { + trustedOrigins.push(ldp.serverUri) + } + // Obtain and store the ACL of the requested resource + const resourceUrl = rootUrl + resourcePath + req.acl = ACL.createFromLDPAndRequest(resourceUrl, ldp, req) -/** - * Returns a fetch document handler used by the ACLChecker to fetch .acl - * resources up the inheritance chain. - * The `fetch(uri, callback)` results in the callback, with either: - * - `callback(err, graph)` if any error is encountered, or - * - `callback(null, graph)` with the parsed RDF graph of the fetched resource - * @return {Function} Returns a `fetch(uri, callback)` handler - */ -function fetchFromLdp (mapper, ldp) { - return function fetch (url, callback) { - // Convert the URL into a filename - mapper.mapUrlToFile({ url }) - // Read the file from disk - .then(({ path }) => new Promise((resolve, reject) => { - ldp.readFile(path, (e, c) => e ? reject(e) : resolve(c)) - })) - // Parse the file as Turtle - .then(body => { - const graph = $rdf.graph() - $rdf.parse(body, graph, url, 'text/turtle') - return graph - }) - // Return the ACL graph - .then(graph => callback(null, graph), callback) + // Ensure the user has the required permission + const userId = req.session.userId + const isAllowed = await req.acl.can(userId, mode) + if (isAllowed) { + return next() + } + if (mode === 'Read' && (resourcePath === '' || resourcePath === '/')) { + // This is a hack to make NSS check the ACL for representation that is served for root (if any) + // See https://github.com/solid/node-solid-server/issues/1063 for more info + const representationUrl = await ldp.resourceMapper.getRepresentationUrlForResource(resourceUrl) + if (representationUrl.endsWith('index.html')) { + // We ONLY want to do this when the representation we return is a HTML file + req.acl = ACL.createFromLDPAndRequest(representationUrl, ldp, req) + const representationIsAllowed = await req.acl.can(userId, mode) + if (representationIsAllowed) { + return next() + } + } + } + const error = await req.acl.getError(userId, mode) + debug(`${mode} access denied to ${userId || '(none)'}: ${error.status} - ${error.message}`) + next(error) } } diff --git a/lib/handlers/copy.js b/lib/handlers/copy.js index 7ff3cca18..4c0012616 100644 --- a/lib/handlers/copy.js +++ b/lib/handlers/copy.js @@ -3,7 +3,6 @@ module.exports = handler const debug = require('../debug') const error = require('../http-error') const ldpCopy = require('../ldp-copy') -const utils = require('../utils') const url = require('url') /** @@ -14,25 +13,25 @@ const url = require('url') * "Save an external resource to Solid" type apps. * @method handler */ -function handler (req, res, next) { +async function handler (req, res, next) { const copyFrom = req.header('Source') if (!copyFrom) { return next(error(400, 'Source header required')) } const fromExternal = !!url.parse(copyFrom).hostname - const serverRoot = utils.getBaseUri(req) + const ldp = req.app.locals.ldp + const serverRoot = ldp.resourceMapper.resolveUrl(req.hostname) const copyFromUrl = fromExternal ? copyFrom : serverRoot + copyFrom - const copyTo = res.locals.path || req.path - const copyToPath = utils.reqToPath(req) - ldpCopy(copyToPath, copyFromUrl, function (err) { - if (err) { - let statusCode = err.statusCode || 500 - let errorMessage = err.statusMessage || err.message - debug.handlers('Error with COPY request:' + errorMessage) - return next(error(statusCode, errorMessage)) - } - res.set('Location', copyTo) - res.sendStatus(201) - next() - }) + const copyToUrl = res.locals.path || req.path + try { + await ldpCopy(ldp.resourceMapper, copyToUrl, copyFromUrl) + } catch (err) { + let statusCode = err.statusCode || 500 + let errorMessage = err.statusMessage || err.message + debug.handlers('Error with COPY request:' + errorMessage) + return next(error(statusCode, errorMessage)) + } + res.set('Location', copyToUrl) + res.sendStatus(201) + next() } diff --git a/lib/handlers/cors-proxy.js b/lib/handlers/cors-proxy.js index ed1719241..c2b7871b5 100644 --- a/lib/handlers/cors-proxy.js +++ b/lib/handlers/cors-proxy.js @@ -11,7 +11,7 @@ const validUrl = require('valid-url') const CORS_SETTINGS = { methods: 'GET', - exposedHeaders: 'Authorization, User, Location, Link, Vary, Last-Modified, Content-Length', + exposedHeaders: 'Authorization, User, Location, Link, Vary, Last-Modified, Content-Length, Content-Location', maxAge: 1728000, origin: true } @@ -19,6 +19,8 @@ const PROXY_SETTINGS = { target: 'dynamic', logLevel: 'silent', changeOrigin: true, + followRedirects: true, + proxyTimeout: 10000, router: req => req.destination.target, pathRewrite: (path, req) => req.destination.path } diff --git a/lib/handlers/delete.js b/lib/handlers/delete.js index 40ced1b84..7454a3c13 100644 --- a/lib/handlers/delete.js +++ b/lib/handlers/delete.js @@ -2,17 +2,17 @@ module.exports = handler const debug = require('../debug').handlers -function handler (req, res, next) { +async function handler (req, res, next) { debug('DELETE -- Request on' + req.originalUrl) const ldp = req.app.locals.ldp - ldp.delete(req.hostname, req.path, function (err) { - if (err) { - debug('DELETE -- Failed to delete: ' + err) - return next(err) - } + try { + await ldp.delete(req) debug('DELETE -- Ok.') res.sendStatus(200) - return next() - }) + next() + } catch (err) { + debug('DELETE -- Failed to delete: ' + err) + next(err) + } } diff --git a/lib/handlers/error-pages.js b/lib/handlers/error-pages.js index dc189997a..413445dfe 100644 --- a/lib/handlers/error-pages.js +++ b/lib/handlers/error-pages.js @@ -32,7 +32,7 @@ function handler (err, req, res, next) { renderLoginRequired(req, res, err) break case 403: - renderNoPermission(req, res) + renderNoPermission(req, res, err) break default: if (ldp.noErrorPages) { @@ -131,10 +131,10 @@ function sendErrorPage (statusCode, res, err, ldp) { * @param req {IncomingRequest} * @param res {ServerResponse} */ -function renderLoginRequired (req, res, error) { +function renderLoginRequired (req, res, err) { const currentUrl = util.fullUrlForReq(req) debug(`Display login-required for ${currentUrl}`) - res.statusMessage = error.statusText + res.statusMessage = err.message res.status(401) res.render('auth/login-required', { currentUrl }) } @@ -145,10 +145,11 @@ function renderLoginRequired (req, res, error) { * @param req {IncomingRequest} * @param res {ServerResponse} */ -function renderNoPermission (req, res) { +function renderNoPermission (req, res, err) { const currentUrl = util.fullUrlForReq(req) const webId = req.session.userId debug(`Display no-permission for ${currentUrl}`) + res.statusMessage = err.message res.status(403) res.render('auth/no-permission', { currentUrl, webId }) } diff --git a/lib/handlers/get.js b/lib/handlers/get.js index d26bc96c6..f9bd0a7ef 100644 --- a/lib/handlers/get.js +++ b/lib/handlers/get.js @@ -5,36 +5,31 @@ const glob = require('glob') const _path = require('path') const $rdf = require('rdflib') const Negotiator = require('negotiator') -const url = require('url') const mime = require('mime-types') const debug = require('debug')('solid:get') const debugGlob = require('debug')('solid:glob') const allow = require('./allow') -const utils = require('../utils.js') const translate = require('../utils.js').translate const error = require('../http-error') -const RDFs = require('../ldp').RDF_MIME_TYPES -const LegacyResourceMapper = require('../legacy-resource-mapper') +const RDFs = require('../ldp').mimeTypesAsArray() -function handler (req, res, next) { +async function handler (req, res, next) { const ldp = req.app.locals.ldp const includeBody = req.method === 'GET' const negotiator = new Negotiator(req) - const baseUri = utils.getFullUri(req) + const baseUri = ldp.resourceMapper.resolveUrl(req.hostname, req.path) const path = res.locals.path || req.path const requestedType = negotiator.mediaType() let possibleRDFType = negotiator.mediaType(RDFs) - // Fallback to text/turtle if content type is unknown - possibleRDFType = (!possibleRDFType) ? 'text/turtle' : possibleRDFType res.header('MS-Author-Via', 'SPARQL') // Set live updates if (ldp.live) { - res.header('Updates-Via', utils.getBaseUri(req).replace(/^http/, 'ws')) + res.header('Updates-Via', ldp.resourceMapper.resolveUrl(req.hostname).replace(/^http/, 'ws')) } debug(req.originalUrl + ' on ' + req.hostname) @@ -42,114 +37,105 @@ function handler (req, res, next) { const options = { 'hostname': req.hostname, 'path': path, - 'baseUri': baseUri, 'includeBody': includeBody, 'possibleRDFType': possibleRDFType, 'range': req.headers.range } - ldp.get(options, function (err, ret) { + + let ret + try { + ret = await ldp.get(options) + } catch (err) { // use globHandler if magic is detected - if (err && err.status === 404 && glob.hasMagic(path)) { + if (err.status === 404 && glob.hasMagic(path)) { debug('forwarding to glob request') return globHandler(req, res, next) - } - - // Handle error - if (err) { + } else { debug(req.method + ' -- Error: ' + err.status + ' ' + err.message) return next(err) } + } - let stream - let contentType - let container - let contentRange - let chunksize - - if (ret) { - stream = ret.stream - contentType = ret.contentType - container = ret.container - contentRange = ret.contentRange - chunksize = ret.chunksize - } - - // Till here it must exist - if (!includeBody) { - debug('HEAD only') - const rootUrl = utils.getBaseUri(req) - const mapper = new LegacyResourceMapper({ - rootUrl, - rootPath: ldp.root, - includeHost: ldp.multiuser - }) - return mapper.mapFileToUrl({ path }).then(mappedFile => { - contentType = mappedFile.contentType - res.setHeader('Content-Type', contentType) - res.status(200).send('OK') - return next() - }) - } + let stream + let contentType + let container + let contentRange + let chunksize + + if (ret) { + stream = ret.stream + contentType = ret.contentType + container = ret.container + contentRange = ret.contentRange + chunksize = ret.chunksize + } - // Handle dataBrowser - if (requestedType && requestedType.includes('text/html')) { - let mimeTypeByExt = mime.lookup(_path.basename(path)) - let isHtmlResource = mimeTypeByExt && mimeTypeByExt.includes('html') - let useDataBrowser = ldp.dataBrowserPath && ( - container || - RDFs.includes(contentType) && !isHtmlResource && !ldp.suppressDataBrowser) - - if (useDataBrowser) { - res.set('Content-Type', 'text/html') - const defaultDataBrowser = _path.join(__dirname, '../../static/databrowser.html') - const dataBrowserPath = ldp.dataBrowserPath === 'default' ? defaultDataBrowser : ldp.dataBrowserPath - debug(' sending data browser file: ' + dataBrowserPath) - res.sendFile(dataBrowserPath) - return - } else if (stream) { - res.setHeader('Content-Type', contentType) - return stream.pipe(res) - } - } + // Till here it must exist + if (!includeBody) { + debug('HEAD only') + res.setHeader('Content-Type', ret.contentType) + return res.status(200).send('OK') + } - // If request accepts the content-type we found - if (stream && negotiator.mediaType([contentType])) { + // Handle dataBrowser + if (requestedType && requestedType.includes('text/html')) { + const { path: filename } = await ldp.resourceMapper.mapUrlToFile({ url: options }) + let mimeTypeByExt = mime.lookup(_path.basename(filename)) + let isHtmlResource = mimeTypeByExt && mimeTypeByExt.includes('html') + let useDataBrowser = ldp.dataBrowserPath && ( + container || + RDFs.includes(contentType) && !isHtmlResource && !ldp.suppressDataBrowser) + + if (useDataBrowser) { + res.set('Content-Type', 'text/html') + const defaultDataBrowser = _path.join(__dirname, '../../static/databrowser.html') + const dataBrowserPath = ldp.dataBrowserPath === 'default' ? defaultDataBrowser : ldp.dataBrowserPath + debug(' sending data browser file: ' + dataBrowserPath) + res.sendFile(dataBrowserPath) + return + } else if (stream) { res.setHeader('Content-Type', contentType) - if (contentRange) { - const headers = { 'Content-Range': contentRange, 'Accept-Ranges': 'bytes', 'Content-Length': chunksize } - res.writeHead(206, headers) - return stream.pipe(res) - } else { - return stream.pipe(res) - } + return stream.pipe(res) } + } - // If it is not in our RDFs we can't even translate, - // Sorry, we can't help - if (!possibleRDFType) { - return next(error(406, 'Cannot serve requested type: ' + contentType)) + // If request accepts the content-type we found + if (stream && negotiator.mediaType([contentType])) { + res.setHeader('Content-Type', contentType) + if (contentRange) { + const headers = { 'Content-Range': contentRange, 'Accept-Ranges': 'bytes', 'Content-Length': chunksize } + res.writeHead(206, headers) + return stream.pipe(res) + } else { + return stream.pipe(res) } + } + // If it is not in our RDFs we can't even translate, + // Sorry, we can't help + if (!possibleRDFType) { + return next(error(406, 'Cannot serve requested type: ' + contentType)) + } + + try { // Translate from the contentType found to the possibleRDFType desired - translate(stream, baseUri, contentType, possibleRDFType, function (err, data) { - if (err) { - debug('error translating: ' + req.originalUrl + ' ' + contentType + ' -> ' + possibleRDFType + ' -- ' + 500 + ' ' + err.message) - return next(error(500, 'Error translating between RDF formats')) - } - debug(req.originalUrl + ' translating ' + contentType + ' -> ' + possibleRDFType) - res.setHeader('Content-Type', possibleRDFType) - res.send(data) - return next() - }) - }) + const data = await translate(stream, baseUri, contentType, possibleRDFType) + debug(req.originalUrl + ' translating ' + contentType + ' -> ' + possibleRDFType) + res.setHeader('Content-Type', possibleRDFType) + res.send(data) + return next() + } catch (err) { + debug('error translating: ' + req.originalUrl + ' ' + contentType + ' -> ' + possibleRDFType + ' -- ' + 500 + ' ' + err.message) + return next(error(500, 'Error translating between RDF formats')) + } } -function globHandler (req, res, next) { +async function globHandler (req, res, next) { const ldp = req.app.locals.ldp - const root = !ldp.multiuser ? ldp.root : ldp.root + req.hostname + '/' - const filename = utils.uriToFilename(req.path, root) - const uri = utils.getFullUri(req) - const requestUri = url.resolve(uri, req.path) + // TODO: This is a hack, that does not check if the target file exists, as this is quite complex with globbing. + // TODO: Proper support for this is not implemented, as globbing support might be removed in the future. + const filename = ldp.resourceMapper.getFullPath(req) + const requestUri = (await ldp.resourceMapper.mapFileToUrl({ path: filename, hostname: req.hostname })).url const globOptions = { noext: true, @@ -166,25 +152,23 @@ function globHandler (req, res, next) { // Matches found const globGraph = $rdf.graph() - let reqOrigin = utils.getBaseUri(req) - debugGlob('found matches ' + matches) - Promise.all(matches.map(match => new Promise((resolve, reject) => { - const baseUri = reqOrigin + '/' + match.substr(root.length) + Promise.all(matches.map(match => new Promise(async (resolve, reject) => { + const urlData = await ldp.resourceMapper.mapFileToUrl({ path: match, hostname: req.hostname }) fs.readFile(match, {encoding: 'utf8'}, function (err, fileData) { if (err) { debugGlob('error ' + err) return resolve() } - // Files should have the .ttl extension or be extensionless (also Turtle) - if (!/\.ttl$|\/[^.]+$/.test(match)) { + // Files should be Turtle + if (urlData.contentType !== 'text/turtle') { return resolve() } // The agent should have Read access to the file hasReadPermissions(match, req, res, function (allowed) { if (allowed) { try { - $rdf.parse(fileData, globGraph, baseUri, 'text/turtle') + $rdf.parse(fileData, globGraph, urlData.url, 'text/turtle') } catch (parseErr) { debugGlob(`error parsing ${match}: ${parseErr}`) } @@ -213,7 +197,7 @@ function hasReadPermissions (file, req, res, callback) { return callback(true) } - const root = ldp.multiuser ? ldp.root + req.hostname + '/' : ldp.root + const root = ldp.resourceMapper.getBasePath(req.hostname) const relativePath = '/' + _path.relative(root, file) res.locals.path = relativePath allow('Read')(req, res, err => callback(!err)) diff --git a/lib/handlers/index.js b/lib/handlers/index.js index 0032f14a0..6c80c6b44 100644 --- a/lib/handlers/index.js +++ b/lib/handlers/index.js @@ -2,27 +2,26 @@ module.exports = handler const path = require('path') const debug = require('debug')('solid:index') -const utils = require('../utils') const Negotiator = require('negotiator') const url = require('url') const URI = require('urijs') -function handler (req, res, next) { +async function handler (req, res, next) { const indexFile = 'index.html' const ldp = req.app.locals.ldp const negotiator = new Negotiator(req) const requestedType = negotiator.mediaType() - const filename = utils.reqToPath(req) - ldp.stat(filename, function (err, stats) { - if (err) return next() + try { + const { path: filename } = await req.app.locals.ldp.resourceMapper.mapUrlToFile({ url: req }) + const stats = await ldp.stat(filename) if (!stats.isDirectory()) { return next() } // redirect to the right container if missing trailing / if (req.path.lastIndexOf('/') !== req.path.length - 1) { - return res.redirect(301, URI.joinPaths(req.path, '/', '//').toString()) + return res.redirect(301, URI.joinPaths(req.path, '/').toString()) } if (requestedType && requestedType.indexOf('text/html') !== 0) { @@ -31,13 +30,11 @@ function handler (req, res, next) { debug('Looking for index in ' + req.path) // Check if file exists in first place - ldp.exists(req.hostname, path.join(req.path, indexFile), function (err) { - if (err) { - return next() - } - res.locals.path = url.resolve(req.path, indexFile) - debug('Found an index for current path') - return next() - }) - }) + await ldp.exists(req.hostname, path.join(req.path, indexFile)) + res.locals.path = url.resolve(req.path, indexFile) + debug('Found an index for current path') + } catch (e) { + // Ignore errors + } + next() } diff --git a/lib/handlers/options.js b/lib/handlers/options.js index b9b8a4099..f658f2c8b 100644 --- a/lib/handlers/options.js +++ b/lib/handlers/options.js @@ -1,5 +1,4 @@ const addLink = require('../header').addLink -const utils = require('../utils') const url = require('url') module.exports = handler @@ -23,7 +22,7 @@ function linkAuthProvider (req, res) { } function linkServiceEndpoint (req, res) { - let serviceEndpoint = url.resolve(utils.getFullUri(req), '.well-known/solid') + let serviceEndpoint = url.resolve(req.app.locals.ldp.resourceMapper.resolveUrl(req.hostname, req.path), '.well-known/solid') addLink(res, serviceEndpoint, 'service') } diff --git a/lib/handlers/patch.js b/lib/handlers/patch.js index c26b59428..18f5823b8 100644 --- a/lib/handlers/patch.js +++ b/lib/handlers/patch.js @@ -3,16 +3,14 @@ module.exports = handler const bodyParser = require('body-parser') -const mime = require('mime-types') const fs = require('fs') const debug = require('../debug').handlers -const utils = require('../utils.js') const error = require('../http-error') const $rdf = require('rdflib') const crypto = require('crypto') const overQuota = require('../utils').overQuota - -const DEFAULT_TARGET_TYPE = 'text/turtle' +const getContentType = require('../utils').getContentType +const { lock } = require('proper-lockfile') // Patch parsers by request body content type const PATCH_PARSERS = { @@ -20,63 +18,80 @@ const PATCH_PARSERS = { 'text/n3': require('./patch/n3-patch-parser.js') } +const DEFAULT_FOR_NEW_CONTENT_TYPE = 'text/turtle' + // Handles a PATCH request -function patchHandler (req, res, next) { +async function patchHandler (req, res, next) { debug(`PATCH -- ${req.originalUrl}`) res.header('MS-Author-Via', 'SPARQL') + let releaseLock + try { + // Obtain details of the target resource + const ldp = req.app.locals.ldp + let path, contentType + try { + // First check if the file already exists + ({ path, contentType } = await ldp.resourceMapper.mapUrlToFile({ url: req })) + } catch (err) { + // If the file doesn't exist, request one to be created with the default content type + ({ path, contentType } = await ldp.resourceMapper.mapUrlToFile( + { url: req, createIfNotExists: true, contentType: DEFAULT_FOR_NEW_CONTENT_TYPE })) + } + const { url } = await ldp.resourceMapper.mapFileToUrl({ path, hostname: req.hostname }) + const resource = { path, contentType, url } + debug('PATCH -- Target <%s> (%s)', url, contentType) + + // Obtain details of the patch document + const patch = {} + patch.text = req.body ? req.body.toString() : '' + patch.uri = `${url}#patch-${hash(patch.text)}` + patch.contentType = getContentType(req.headers) + debug('PATCH -- Received patch (%d bytes, %s)', patch.text.length, patch.contentType) + const parsePatch = PATCH_PARSERS[patch.contentType] + if (!parsePatch) { + throw error(415, `Unsupported patch content type: ${patch.contentType}`) + } - // Obtain details of the target resource - const ldp = req.app.locals.ldp - const root = !ldp.multiuser ? ldp.root : `${ldp.root}${req.hostname}/` - const target = {} - target.file = utils.uriToFilename(req.path, root) - target.uri = utils.getBaseUri(req) + req.originalUrl - target.contentType = mime.lookup(target.file) || DEFAULT_TARGET_TYPE - debug('PATCH -- Target <%s> (%s)', target.uri, target.contentType) - - // Obtain details of the patch document - const patch = {} - patch.text = req.body ? req.body.toString() : '' - patch.uri = `${target.uri}#patch-${hash(patch.text)}` - patch.contentType = (req.get('content-type') || '').match(/^[^;\s]*/)[0] - debug('PATCH -- Received patch (%d bytes, %s)', patch.text.length, patch.contentType) - const parsePatch = PATCH_PARSERS[patch.contentType] - if (!parsePatch) { - return next(error(415, `Unsupported patch content type: ${patch.contentType}`)) + // Parse the patch document and verify permissions + const patchObject = await parsePatch(url, patch.uri, patch.text) + await checkPermission(req, patchObject) + + // Patch the graph and write it back to the file + releaseLock = await lock(path, { retries: 10, realpath: false }) + const graph = await readGraph(resource) + await applyPatch(patchObject, graph, url) + const result = await writeGraph(graph, resource, ldp.resourceMapper.rootPath, ldp.serverUri) + + // Send the result to the client + res.send(result) + } catch (err) { + return next(err) + } finally { + if (releaseLock) { + await releaseLock() + } } - - // Parse the target graph and the patch document, - // and verify permission for performing this specific patch - Promise.all([ - readGraph(target), - parsePatch(target.uri, patch.uri, patch.text) - .then(patchObject => checkPermission(target, req, patchObject)) - ]) - // Patch the graph and write it back to the file - .then(([graph, patchObject]) => applyPatch(patchObject, graph, target)) - .then(graph => writeGraph(graph, target, root, ldp.serverUri)) - // Send the result to the client - .then(result => { res.send(result) }) - .then(next, next) + return next() } // Reads the request body and calls the actual patch handler function handler (req, res, next) { readEntity(req, res, () => patchHandler(req, res, next)) } + const readEntity = bodyParser.text({ type: () => true }) // Reads the RDF graph in the given resource function readGraph (resource) { // Read the resource's file return new Promise((resolve, reject) => - fs.readFile(resource.file, {encoding: 'utf8'}, function (err, fileContents) { + fs.readFile(resource.path, { encoding: 'utf8' }, function (err, fileContents) { if (err) { // If the file does not exist, assume empty contents // (it will be created after a successful patch) if (err.code === 'ENOENT') { fileContents = '' - // Fail on all other errors + // Fail on all other errors } else { return reject(error(500, `Original file read error: ${err}`)) } @@ -88,9 +103,9 @@ function readGraph (resource) { // Parse the resource's file contents .then((fileContents) => { const graph = $rdf.graph() - debug('PATCH -- Reading %s with content type %s', resource.uri, resource.contentType) + debug('PATCH -- Reading %s with content type %s', resource.url, resource.contentType) try { - $rdf.parse(fileContents, graph, resource.uri, resource.contentType) + $rdf.parse(fileContents, graph, resource.url, resource.contentType) } catch (err) { throw error(500, `Patch: Target ${resource.contentType} file syntax error: ${err}`) } @@ -100,14 +115,14 @@ function readGraph (resource) { } // Verifies whether the user is allowed to perform the patch on the target -function checkPermission (target, request, patchObject) { +async function checkPermission (request, patchObject) { // If no ACL object was passed down, assume permissions are okay. if (!request.acl) return Promise.resolve(patchObject) // At this point, we already assume append access, // as this can be checked upfront before parsing the patch. // Now that we know the details of the patch, // we might need to perform additional checks. - let checks = [] + let modes = [] const { acl, session: { userId } } = request // Read access is required for DELETE and WHERE. // If we would allows users without read access, @@ -115,18 +130,28 @@ function checkPermission (target, request, patchObject) { // and thereby guess the existence of certain triples. // DELETE additionally requires write access. if (patchObject.delete) { - checks = [acl.can(userId, 'Read'), acl.can(userId, 'Write')] + modes = ['Read', 'Write'] + // checks = [acl.can(userId, 'Read'), acl.can(userId, 'Write')] } else if (patchObject.where) { - checks = [acl.can(userId, 'Read')] + modes = ['Read'] + // checks = [acl.can(userId, 'Read')] + } + const allowed = await Promise.all(modes.map(mode => acl.can(userId, mode))) + const allAllowed = allowed.reduce((memo, allowed) => memo && allowed, true) + if (!allAllowed) { + const errors = await Promise.all(modes.map(mode => acl.getError(userId, mode))) + const error = errors.filter(error => !!error) + .reduce((prevErr, err) => prevErr.status > err.status ? prevErr : err, { status: 0 }) + return Promise.reject(error) } - return Promise.all(checks).then(() => patchObject) + return Promise.resolve(patchObject) } // Applies the patch to the RDF graph -function applyPatch (patchObject, graph, target) { +function applyPatch (patchObject, graph, url) { debug('PATCH -- Applying patch') return new Promise((resolve, reject) => - graph.applyPatch(patchObject, graph.sym(target.uri), (err) => { + graph.applyPatch(patchObject, graph.sym(url), (err) => { if (err) { const message = err.message || err // returns string at the moment debug(`PATCH -- FAILED. Returning 409. Message: '${message}'`) @@ -141,8 +166,8 @@ function applyPatch (patchObject, graph, target) { function writeGraph (graph, resource, root, serverUri) { debug('PATCH -- Writing patched file') return new Promise((resolve, reject) => { - const resourceSym = graph.sym(resource.uri) - const serialized = $rdf.serialize(resourceSym, graph, resource.uri, resource.contentType) + const resourceSym = graph.sym(resource.url) + const serialized = $rdf.serialize(resourceSym, graph, resource.url, resource.contentType) // First check if we are above quota overQuota(root, serverUri).then((isOverQuota) => { @@ -151,7 +176,7 @@ function writeGraph (graph, resource, root, serverUri) { 'User has exceeded their storage quota')) } - fs.writeFile(resource.file, serialized, {encoding: 'utf8'}, function (err) { + fs.writeFile(resource.path, serialized, { encoding: 'utf8' }, function (err) { if (err) { return reject(error(500, `Failed to write file after patch: ${err}`)) } diff --git a/lib/handlers/patch/n3-patch-parser.js b/lib/handlers/patch/n3-patch-parser.js index 91df7c5b6..6f297b6cb 100644 --- a/lib/handlers/patch/n3-patch-parser.js +++ b/lib/handlers/patch/n3-patch-parser.js @@ -9,34 +9,35 @@ const PATCH_NS = 'http://www.w3.org/ns/solid/terms#' const PREFIXES = `PREFIX solid: <${PATCH_NS}>\n` // Parses the given N3 patch document -function parsePatchDocument (targetURI, patchURI, patchText) { +async function parsePatchDocument (targetURI, patchURI, patchText) { // Parse the N3 document into triples - return new Promise((resolve, reject) => { - const patchGraph = $rdf.graph() + const patchGraph = $rdf.graph() + try { $rdf.parse(patchText, patchGraph, patchURI, 'text/n3') - resolve(patchGraph) - }) - .catch(err => { throw error(400, `Patch document syntax error: ${err}`) }) + } catch (err) { + throw error(400, `Patch document syntax error: ${err}`) + } // Query the N3 document for insertions and deletions - .then(patchGraph => queryForFirstResult(patchGraph, `${PREFIXES} + let firstResult + try { + firstResult = await queryForFirstResult(patchGraph, `${PREFIXES} SELECT ?insert ?delete ?where WHERE { ?patch solid:patches <${targetURI}>. OPTIONAL { ?patch solid:inserts ?insert. } OPTIONAL { ?patch solid:deletes ?delete. } OPTIONAL { ?patch solid:where ?where. } }`) - .catch(err => { throw error(400, `No patch for ${targetURI} found.`, err) }) - ) + } catch (err) { + throw error(400, `No patch for ${targetURI} found.`, err) + } // Return the insertions and deletions as an rdflib patch document - .then(result => { - const {'?insert': insert, '?delete': deleted, '?where': where} = result - if (!insert && !deleted) { - throw error(400, 'Patch should at least contain inserts or deletes.') - } - return {insert, delete: deleted, where} - }) + const {'?insert': insert, '?delete': deleted, '?where': where} = firstResult + if (!insert && !deleted) { + throw error(400, 'Patch should at least contain inserts or deletes.') + } + return {insert, delete: deleted, where} } // Queries the store with the given SPARQL query and returns the first result diff --git a/lib/handlers/patch/sparql-update-parser.js b/lib/handlers/patch/sparql-update-parser.js index 365e9c82d..8e442c9ed 100644 --- a/lib/handlers/patch/sparql-update-parser.js +++ b/lib/handlers/patch/sparql-update-parser.js @@ -6,13 +6,11 @@ const $rdf = require('rdflib') const error = require('../../http-error') // Parses the given SPARQL UPDATE document -function parsePatchDocument (targetURI, patchURI, patchText) { - return new Promise((resolve, reject) => { - const baseURI = patchURI.replace(/#.*/, '') - try { - resolve($rdf.sparqlUpdateParser(patchText, $rdf.graph(), baseURI)) - } catch (err) { - reject(error(400, `Patch document syntax error: ${err}`)) - } - }) +async function parsePatchDocument (targetURI, patchURI, patchText) { + const baseURI = patchURI.replace(/#.*/, '') + try { + return $rdf.sparqlUpdateParser(patchText, $rdf.graph(), baseURI) + } catch (err) { + throw error(400, `Patch document syntax error: ${err}`) + } } diff --git a/lib/handlers/post.js b/lib/handlers/post.js index 5480c6bba..928528417 100644 --- a/lib/handlers/post.js +++ b/lib/handlers/post.js @@ -7,10 +7,11 @@ const header = require('../header') const patch = require('./patch') const error = require('../http-error') const { extensions } = require('mime-types') +const getContentType = require('../utils').getContentType -function handler (req, res, next) { +async function handler (req, res, next) { const ldp = req.app.locals.ldp - const contentType = req.get('content-type') + const contentType = getContentType(req.headers) debug('content-type is ', contentType) // Handle SPARQL(-update?) query if (contentType === 'application/sparql' || @@ -26,45 +27,42 @@ function handler (req, res, next) { } // Check if container exists - ldp.exists(req.hostname, containerPath, function (err, ret) { - if (err) { - return next(error(err, 'Container not valid')) - } - - let stats + let stats + try { + const ret = await ldp.exists(req.hostname, containerPath, false) if (ret) { stats = ret.stream } + } catch (err) { + return next(error(err, 'Container not valid')) + } - // Check if container is a directory - if (stats && !stats.isDirectory()) { - debug('Path is not a container, 405!') - return next(error(405, 'Requested resource is not a container')) - } + // Check if container is a directory + if (stats && !stats.isDirectory()) { + debug('Path is not a container, 405!') + return next(error(405, 'Requested resource is not a container')) + } - // Dispatch to the right handler - if (req.is('multipart/form-data')) { - multi() - } else { - one() - } - }) + // Dispatch to the right handler + if (req.is('multipart/form-data')) { + multi() + } else { + one() + } function multi () { debug('receving multiple files') const busboy = new Busboy({ headers: req.headers }) - busboy.on('file', function (fieldname, file, filename, encoding, mimetype) { + busboy.on('file', async function (fieldname, file, filename, encoding, mimetype) { debug('One file received via multipart: ' + filename) - ldp.put( - req.hostname, - path.join(containerPath, filename), - file, - function (err) { - if (err) { - busboy.emit('error', err) - } - }) + const { url: putUrl } = await ldp.resourceMapper.mapFileToUrl( + { path: ldp.resourceMapper._rootPath + path.join(containerPath, filename), hostname: req.hostname }) + try { + await ldp.put(putUrl, file, mimetype) + } catch (err) { + busboy.emit('error', err) + } }) busboy.on('error', function (err) { debug('Error receiving the file: ' + err.message) @@ -88,16 +86,14 @@ function handler (req, res, next) { const extension = mimeType in extensions ? `.${extensions[mimeType][0]}` : '' ldp.post(req.hostname, containerPath, req, - { slug, extension, container: links.isBasicContainer }, - function (err, resourcePath) { - if (err) { - return next(err) - } + { slug, extension, container: links.isBasicContainer }).then( + resourcePath => { debug('File stored in ' + resourcePath) header.addLinks(res, links) res.set('Location', resourcePath) res.sendStatus(201) next() - }) + }, + err => next(err)) } } diff --git a/lib/handlers/put.js b/lib/handlers/put.js index 1dd2f550d..a4e1e24db 100644 --- a/lib/handlers/put.js +++ b/lib/handlers/put.js @@ -1,22 +1,51 @@ module.exports = handler +const bodyParser = require('body-parser') const debug = require('debug')('solid:put') +const getContentType = require('../utils').getContentType +const HTTPError = require('../http-error') +const { stringToStream } = require('../utils') +const LDP = require('../ldp') -function handler (req, res, next) { - const ldp = req.app.locals.ldp +async function handler (req, res, next) { debug(req.originalUrl) res.header('MS-Author-Via', 'SPARQL') - ldp.put(req.hostname, req.path, req, function (err) { - if (err) { - debug('error putting the file:' + err.message) - err.message = 'Can\'t write file: ' + err.message - return next(err) - } + const contentType = req.get('content-type') + if (LDP.mimeTypeIsRdf(contentType) && isAclFile(req)) { + return bodyParser.text({ type: () => true })(req, res, () => putAcl(req, res, next)) + } + return putStream(req, res, next) +} +async function putStream (req, res, next, stream = req) { + const ldp = req.app.locals.ldp + try { + await ldp.put(req, stream, getContentType(req.headers)) debug('succeded putting the file') res.sendStatus(201) return next() - }) + } catch (err) { + debug('error putting the file:' + err.message) + err.message = 'Can\'t write file: ' + err.message + return next(err) + } +} + +async function putAcl (req, res, next) { + const ldp = req.app.locals.ldp + const contentType = req.get('content-type') + const path = ldp.resourceMapper.getFullPath(req) + const requestUri = await ldp.resourceMapper.resolveUrl(req.hostname, path) + if (ldp.isValidRdf(req.body, requestUri, contentType)) { + const stream = stringToStream(req.body) + return putStream(req, res, next, stream) + } + next(new HTTPError(400, 'RDF file contains invalid syntax')) +} + +function isAclFile (req) { + const originalUrlParts = req.originalUrl.split('.') + return originalUrlParts[originalUrlParts.length - 1] === 'acl' } diff --git a/lib/handlers/restrict-to-top-domain.js b/lib/handlers/restrict-to-top-domain.js new file mode 100644 index 000000000..2b6aecc37 --- /dev/null +++ b/lib/handlers/restrict-to-top-domain.js @@ -0,0 +1,13 @@ +const HTTPError = require('../http-error') + +module.exports = function (req, res, next) { + const locals = req.app.locals + const ldp = locals.ldp + const serverUri = locals.host.serverUri + const hostname = ldp.resourceMapper.resolveUrl(req.hostname) + if (hostname === serverUri) { + return next() + } + const isLoggedIn = !!(req.session && req.session.userId) + return next(new HTTPError(isLoggedIn ? 403 : 401, 'Not allowed to access top-level APIs on accounts')) +} diff --git a/lib/header.js b/lib/header.js index aaeab6f19..eeb8ff61a 100644 --- a/lib/header.js +++ b/lib/header.js @@ -41,19 +41,29 @@ function addLinks (res, fileMetadata) { } } -function linksHandler (req, res, next) { +async function linksHandler (req, res, next) { const ldp = req.app.locals.ldp - const root = !ldp.multiuser ? ldp.root : ldp.root + req.hostname + '/' - let filename = utils.uriToFilename(req.url, root) + let filename + try { + // Hack: createIfNotExists is set to true for PUT or PATCH requests + // because the file might not exist yet at this point. + // But it will be created afterwards. + // This should be improved with the new server architecture. + ({ path: filename } = await ldp.resourceMapper + .mapUrlToFile({ url: req, createIfNotExists: req.method === 'PUT' || req.method === 'PATCH' })) + } catch (e) { + // Silently ignore errors here + // Later handlers will error as well, but they will be able to given a more concrete error message (like 400 or 404) + return next() + } - filename = path.join(filename, req.path) if (path.extname(filename) === ldp.suffixMeta) { debug.metadata('Trying to access metadata file as regular file.') return next(error(404, 'Trying to access metadata file as regular file')) } let fileMetadata = new metadata.Metadata() - if (req.path.endsWith('/')) { + if (filename.endsWith('/')) { fileMetadata.isContainer = true fileMetadata.isBasicContainer = true } else { @@ -100,26 +110,25 @@ function parseMetadataFromHeader (linkHeader) { } // Adds a header that describes the user's permissions -function addPermissions (req, res, next) { +async function addPermissions (req, res, next) { const { acl, session } = req if (!acl) return next() // Turn permissions for the public and the user into a header - const resource = utils.getFullUri(req) - Promise.all([ - getPermissionsFor(acl, null, resource), - getPermissionsFor(acl, session.userId, resource) + const resource = req.app.locals.ldp.resourceMapper.resolveUrl(req.hostname, req.path) + const [publicPerms, userPerms] = await Promise.all([ + getPermissionsFor(acl, null, req), + getPermissionsFor(acl, session.userId, req) ]) - .then(([publicPerms, userPerms]) => { - debug.ACL(`Permissions on ${resource} for ${session.userId || '(none)'}: ${userPerms}`) - debug.ACL(`Permissions on ${resource} for public: ${publicPerms}`) - res.set('WAC-Allow', `user="${userPerms}",public="${publicPerms}"`) - }) - .then(next, next) + debug.ACL(`Permissions on ${resource} for ${session.userId || '(none)'}: ${userPerms}`) + debug.ACL(`Permissions on ${resource} for public: ${publicPerms}`) + res.set('WAC-Allow', `user="${userPerms}",public="${publicPerms}"`) + next() } // Gets the permissions string for the given user and resource -function getPermissionsFor (acl, user, resource) { - return Promise.all(MODES.map(mode => acl.can(user, mode).catch(e => false))) - .then(allowed => PERMISSIONS.filter((_, i) => allowed[i]).join(' ')) +async function getPermissionsFor (acl, user, req) { + const accesses = MODES.map(mode => acl.can(user, mode)) + const allowed = await Promise.all(accesses) + return PERMISSIONS.filter((mode, i) => allowed[i]).join(' ') } diff --git a/lib/ldp-container.js b/lib/ldp-container.js index c290879af..d075c1ea9 100644 --- a/lib/ldp-container.js +++ b/lib/ldp-container.js @@ -1,7 +1,6 @@ module.exports.addContainerStats = addContainerStats module.exports.addFile = addFile module.exports.addStats = addStats -module.exports.getMetadataGraph = getMetadataGraph module.exports.readdir = readdir const $rdf = require('rdflib') @@ -12,109 +11,102 @@ const ns = require('solid-namespace')($rdf) const mime = require('mime-types') const path = require('path') -function addContainerStats (ldp, reqUri, filename, resourceGraph, next) { - ldp.stat(filename, function (err, containerStats) { - if (!err) { - addStats(resourceGraph, reqUri, containerStats) - resourceGraph.add( - resourceGraph.sym(reqUri), - ns.rdf('type'), - ns.ldp('BasicContainer')) - - resourceGraph.add( - resourceGraph.sym(reqUri), - ns.rdf('type'), - ns.ldp('Container')) - } - next() - }) +async function addContainerStats (ldp, reqUri, filename, resourceGraph) { + const containerStats = await ldp.stat(filename) + addStats(resourceGraph, reqUri, containerStats, filename) + resourceGraph.add( + resourceGraph.sym(reqUri), + ns.rdf('type'), + ns.ldp('BasicContainer')) + resourceGraph.add( + resourceGraph.sym(reqUri), + ns.rdf('type'), + ns.ldp('Container')) } -function addFile (ldp, resourceGraph, containerUri, reqUri, uri, container, file, callback) { +async function addFile (ldp, resourceGraph, containerUri, reqUri, container, file) { // Skip .meta and .acl if (file.endsWith(ldp.suffixMeta) || file.endsWith(ldp.suffixAcl)) { - return callback(null) + return null } const filePath = path.join(container, file) // Get file stats - ldp.stat(filePath, function (err, stats) { - if (err) { - // File does not exist, skip - return callback(null) - } + let stats + try { + stats = await ldp.stat(filePath) + } catch (e) { + return null + } + let memberUri = reqUri + (stats.isDirectory() ? '/' : '') - let memberUri = reqUri + (stats.isDirectory() ? '/' : '') - // var fileSubject = file + (stats.isDirectory() ? '/' : '') - // var fileBaseUri = utils.filenameToBaseUri(fileSubject, uri, root) + // Add fileStats to resource Graph + addStats(resourceGraph, memberUri, stats, file) - // Add fileStats to resource Graph - addStats(resourceGraph, memberUri, stats) + // Add to `contains` list + resourceGraph.add( + resourceGraph.sym(containerUri), + ns.ldp('contains'), + resourceGraph.sym(memberUri)) + + // Set up a metaFile path + // Earlier code used a .ttl file as its own meta file, which + // caused massive data files to parsed as part of deirectory listings just looking for type triples + const metaFile = containerUri + file + ldp.suffixMeta + + let metadataGraph + try { + metadataGraph = await getMetadataGraph(ldp, metaFile, memberUri) + } catch (err) { + metadataGraph = $rdf.graph() + } - // Add to `contains` list + // Add Container or BasicContainer types + if (stats.isDirectory()) { resourceGraph.add( - resourceGraph.sym(containerUri), - ns.ldp('contains'), - resourceGraph.sym(memberUri)) - - // Set up a metaFile path - // Earlier code used a .ttl file as its own meta file, which - // caused massive data files to parsed as part of deirectory listings just looking for type triples - const metaFile = container + file + ldp.suffixMeta - - getMetadataGraph(ldp, metaFile, memberUri, function (err, metadataGraph) { - if (err) { - metadataGraph = $rdf.graph() - } - - // Add Container or BasicContainer types - if (stats.isDirectory()) { - resourceGraph.add( - metadataGraph.sym(memberUri), - ns.rdf('type'), - ns.ldp('BasicContainer')) + metadataGraph.sym(memberUri), + ns.rdf('type'), + ns.ldp('BasicContainer')) + resourceGraph.add( + metadataGraph.sym(memberUri), + ns.rdf('type'), + ns.ldp('Container')) + } + // Add generic LDP type + resourceGraph.add( + metadataGraph.sym(memberUri), + ns.rdf('type'), + ns.ldp('Resource')) + + // Add type from metadataGraph + metadataGraph + .statementsMatching( + metadataGraph.sym(memberUri), + ns.rdf('type'), + undefined) + .forEach(function (typeStatement) { + // If the current is a file and its type is BasicContainer, + // This is not possible, so do not infer its type! + if ( + ( + typeStatement.object.uri !== ns.ldp('BasicContainer').uri && + typeStatement.object.uri !== ns.ldp('Container').uri + ) || + !stats.isFile() + ) { resourceGraph.add( - metadataGraph.sym(memberUri), - ns.rdf('type'), - ns.ldp('Container')) + resourceGraph.sym(reqUri), + typeStatement.predicate, + typeStatement.object) } - // Add generic LDP type - resourceGraph.add( - metadataGraph.sym(memberUri), - ns.rdf('type'), - ns.ldp('Resource')) - - // Add type from metadataGraph - metadataGraph - .statementsMatching( - metadataGraph.sym(memberUri), - ns.rdf('type'), - undefined) - .forEach(function (typeStatement) { - // If the current is a file and its type is BasicContainer, - // This is not possible, so do not infer its type! - if ( - ( - typeStatement.object.uri !== ns.ldp('BasicContainer').uri && - typeStatement.object.uri !== ns.ldp('Container').uri - ) || - !stats.isFile() - ) { - resourceGraph.add( - resourceGraph.sym(reqUri), - typeStatement.predicate, - typeStatement.object) - } - }) - - return callback(null) }) - }) + + return null } -function addStats (resourceGraph, reqUri, stats) { +function addStats (resourceGraph, reqUri, stats, filename) { resourceGraph.add( resourceGraph.sym(reqUri), ns.stat('mtime'), // Deprecate? @@ -130,8 +122,8 @@ function addStats (resourceGraph, reqUri, stats) { ns.stat('size'), stats.size) - if (mime.lookup(reqUri)) { // Is the file has a well-known type, - let type = 'http://www.w3.org/ns/iana/media-types/' + mime.lookup(reqUri) + '#Resource' + if (mime.lookup(filename)) { // Is the file has a well-known type, + let type = 'http://www.w3.org/ns/iana/media-types/' + mime.lookup(filename) + '#Resource' resourceGraph.add( resourceGraph.sym(reqUri), ns.rdf('type'), // convert MIME type to RDF @@ -140,45 +132,30 @@ function addStats (resourceGraph, reqUri, stats) { } } -function readdir (filename, callback) { +function readdir (filename) { debug.handlers('GET -- Reading directory') - fs.readdir(filename, function (err, files) { - if (err) { - debug.handlers('GET -- Error reading files: ' + err) - return callback(error(err, 'Can\'t read container')) - } + return new Promise((resolve, reject) => { + fs.readdir(filename, function (err, files) { + if (err) { + debug.handlers('GET -- Error reading files: ' + err) + return reject(error(err, 'Can\'t read container')) + } - debug.handlers('Files in directory: ' + files.toString().slice(0, 100)) - return callback(null, files) + debug.handlers('Files in directory: ' + files.toString().slice(0, 100)) + return resolve(files) + }) }) } -function getMetadataGraph (ldp, metaFile, fileBaseUri, callback) { - ldp.stat(metaFile, function (err, metaStats) { - if (err) { - return callback(err) +async function getMetadataGraph (ldp, metaFile) { + const metaStats = await ldp.stat(metaFile) + if (metaStats && metaStats.isFile()) { + try { + return await ldp.getGraph(metaFile) + } catch (err) { + throw error(err, 'Can\'t parse container metadata') } - - if (metaStats && metaStats.isFile()) { - ldp.readFile(metaFile, function (err, rawMetadata) { - if (err) { - return callback(err) - } - - const metadataGraph = $rdf.graph() - try { - $rdf.parse( - rawMetadata, - metadataGraph, - fileBaseUri, - 'text/turtle') - } catch (dirErr) { - return callback(error(err, 'Can\'t parse container metadata')) - } - return callback(null, metadataGraph) - }) - } else { - return callback(null, $rdf.graph()) - } - }) + } else { + return $rdf.graph() + } } diff --git a/lib/ldp-copy.js b/lib/ldp-copy.js index a9e1ccf19..c9ebad372 100644 --- a/lib/ldp-copy.js +++ b/lib/ldp-copy.js @@ -3,9 +3,11 @@ module.exports = copy const debug = require('./debug') const fs = require('fs') const mkdirp = require('fs-extra').mkdirp +const error = require('./http-error') const path = require('path') const http = require('http') const https = require('https') +const getContentType = require('./utils').getContentType /** * Cleans up a file write stream (ends stream, deletes the file). @@ -21,47 +23,51 @@ function cleanupFileStream (stream) { /** * Performs an LDP Copy operation, imports a remote resource to a local path. - * @param copyToPath {String} Local path to copy the resource into + * @param resourceMapper {ResourceMapper} A resource mapper instance. + * @param copyToUri {Object} The location (in the current domain) to copy to. * @param copyFromUri {String} Location of remote resource to copy from - * @param callback {Function} Node error callback + * @return A promise resolving when the copy operation is finished */ -function copy (copyToPath, copyFromUri, callback) { - mkdirp(path.dirname(copyToPath), function (err) { - if (err) { - debug.handlers('COPY -- Error creating destination directory: ' + err) - return callback( - new Error('Failed to create the path to the destination resource: ' + - err)) - } - const destinationStream = fs.createWriteStream(copyToPath) - .on('error', function (err) { - cleanupFileStream(this) - return callback(new Error('Error writing data: ' + err)) - }) - .on('finish', function () { - // Success - debug.handlers('COPY -- Wrote data to: ' + copyToPath) - callback() - }) +function copy (resourceMapper, copyToUri, copyFromUri) { + return new Promise((resolve, reject) => { const request = /^https:/.test(copyFromUri) ? https : http request.get(copyFromUri) .on('error', function (err) { debug.handlers('COPY -- Error requesting source file: ' + err) this.end() - cleanupFileStream(destinationStream) - return callback(new Error('Error writing data: ' + err)) + return reject(new Error('Error writing data: ' + err)) }) .on('response', function (response) { if (response.statusCode !== 200) { - debug.handlers('COPY -- HTTP error reading source file: ' + - response.statusMessage) + debug.handlers('COPY -- HTTP error reading source file: ' + response.statusMessage) this.end() - cleanupFileStream(destinationStream) let error = new Error('Error reading source file: ' + response.statusMessage) error.statusCode = response.statusCode - return callback(error) + return reject(error) } - response.pipe(destinationStream) + // Grab the content type from the source + const contentType = getContentType(response.headers) + resourceMapper.mapUrlToFile({ url: copyToUri, createIfNotExists: true, contentType }) + .then(({ path: copyToPath }) => { + mkdirp(path.dirname(copyToPath), function (err) { + if (err) { + debug.handlers('COPY -- Error creating destination directory: ' + err) + return reject(new Error('Failed to create the path to the destination resource: ' + err)) + } + const destinationStream = fs.createWriteStream(copyToPath) + .on('error', function (err) { + cleanupFileStream(this) + return reject(new Error('Error writing data: ' + err)) + }) + .on('finish', function () { + // Success + debug.handlers('COPY -- Wrote data to: ' + copyToPath) + resolve() + }) + response.pipe(destinationStream) + }) + }) + .catch(() => reject(error(500, 'Could not find target file to copy'))) }) }) } diff --git a/lib/ldp-middleware.js b/lib/ldp-middleware.js index 1b35d56ad..26f32f45d 100644 --- a/lib/ldp-middleware.js +++ b/lib/ldp-middleware.js @@ -26,7 +26,7 @@ function LdpMiddleware (corsSettings) { router.post('/*', allow('Append'), post) router.patch('/*', allow('Append'), patch) router.put('/*', allow('Write'), put) - router.delete('/*', allow('Write'), del) + router.delete('/*', allow('Write', true), del) return router } diff --git a/lib/ldp.js b/lib/ldp.js index ead94b321..6621f0a1e 100644 --- a/lib/ldp.js +++ b/lib/ldp.js @@ -1,16 +1,15 @@ -const mime = require('mime-types') -const path = require('path') +const { join, dirname } = require('path') const url = require('url') const fs = require('fs') const $rdf = require('rdflib') const mkdirp = require('fs-extra').mkdirp const uuid = require('uuid') const debug = require('./debug') -const utils = require('./utils') const error = require('./http-error') const stringToStream = require('./utils').stringToStream const serialize = require('./utils').serialize const overQuota = require('./utils').overQuota +const getContentType = require('./utils').getContentType const extend = require('extend') const rimraf = require('rimraf') const ldpContainer = require('./ldp-container') @@ -18,10 +17,9 @@ const parse = require('./utils').parse const fetch = require('node-fetch') const { promisify } = require('util') const URI = require('urijs') +const { lock } = require('proper-lockfile') -const DEFAULT_CONTENT_TYPE = 'text/turtle' - -const RDF_MIME_TYPES = [ +const RDF_MIME_TYPES = new Set([ 'text/turtle', // .ttl 'text/n3', // .n3 'text/html', // RDFa @@ -32,20 +30,12 @@ const RDF_MIME_TYPES = [ 'application/rdf+xml', // .rdf 'application/ld+json', // .jsonld 'application/x-turtle' -] +]) class LDP { constructor (argv = {}) { extend(this, argv) - // Setting root - if (!this.root) { - this.root = process.cwd() - } - if (!this.root.endsWith('/')) { - this.root += '/' - } - // Suffixes if (!this.suffixAcl) { this.suffixAcl = '.acl' @@ -53,7 +43,6 @@ class LDP { if (!this.suffixMeta) { this.suffixMeta = '.meta' } - this.turtleExtensions = [ '.ttl', this.suffixAcl, this.suffixMeta ] // Error pages folder this.errorPages = null @@ -75,127 +64,83 @@ class LDP { this.corsProxy = '/' + this.corsProxy } - debug.settings('Server URI: ' + this.serverUri) - debug.settings('Auth method: ' + this.auth) - debug.settings('Db path: ' + this.dbPath) - debug.settings('Config path: ' + this.configPath) - debug.settings('Suffix Acl: ' + this.suffixAcl) - debug.settings('Suffix Meta: ' + this.suffixMeta) - debug.settings('Filesystem Root: ' + this.root) - debug.settings('Allow WebID authentication: ' + !!this.webid) - debug.settings('Live-updates: ' + !!this.live) - debug.settings('Multi-user: ' + !!this.multiuser) - debug.settings('Suppress default data browser app: ' + this.suppressDataBrowser) - debug.settings('Default data browser app file path: ' + this.dataBrowserPath) - return this } - stat (file, callback) { - fs.stat(file, function (err, stats) { - if (err) { - return callback(error(err, "Can't read metadata")) - } - return callback(null, stats) + async stat (file) { + return new Promise((resolve, reject) => { + fs.stat(file, (err, stats) => { + if (err) return reject(error(err, "Can't read metadata of " + file)) + resolve(stats) + }) }) } - createReadStream (filename, start, end) { - if (start && end) { - return fs.createReadStream(filename, {'start': start, 'end': end}) - } else { - return fs.createReadStream(filename) + async readResource (url) { + let releaseLock + try { + const { path } = await this.resourceMapper.mapUrlToFile({ url }) + releaseLock = await lock(path, { retries: 10 }) + return await promisify(fs.readFile)(path, {encoding: 'utf8'}) + } catch (err) { + throw error(err.status, err.message) + } finally { + if (releaseLock) { + await releaseLock() + } } } - readFile (filename, callback) { - fs.readFile( - filename, - { 'encoding': 'utf8' }, - function (err, data) { - if (err) { - return callback(error(err, "Can't read file")) - } - return callback(null, data) - }) - } - - readContainerMeta (directory, callback) { - const ldp = this - - if (directory[ directory.length - 1 ] !== '/') { - directory += '/' + async readContainerMeta (url) { + if (url[ url.length - 1 ] !== '/') { + url += '/' } - - ldp.readFile(directory + ldp.suffixMeta, function (err, data) { - if (err) { - return callback(error(err, "Can't read meta file")) - } - - return callback(null, data) - }) + return this.readResource(url + this.suffixMeta) } - listContainer (filename, reqUri, uri, containerData, contentType, callback) { - const ldp = this - // var host = url.parse(uri).hostname - // var root = !ldp.multiuser ? ldp.root : ldp.root + host + '/' - - // var baseUri = utils.filenameToBaseUri(filename, uri, root) + async listContainer (container, reqUri, containerData, hostname) { const resourceGraph = $rdf.graph() try { $rdf.parse(containerData, resourceGraph, reqUri, 'text/turtle') } catch (err) { debug.handlers('GET -- Error parsing data: ' + err) - return callback(error(500, "Can't parse container")) - } - - // add container stats - new Promise((resolve, reject) => - ldpContainer.addContainerStats(ldp, reqUri, filename, resourceGraph, - err => err ? reject(err) : resolve()) - ) - // read directory - .then(() => new Promise((resolve, reject) => - ldpContainer.readdir(filename, - (err, files) => err ? reject(err) : resolve(files)) - )) - // iterate through all the files - .then(files => { - return Promise.all(files.map(file => - new Promise((resolve, reject) => { - const fileUri = url.resolve(reqUri, encodeURIComponent(file)) - ldpContainer.addFile(ldp, resourceGraph, reqUri, fileUri, uri, - filename, file, err => err ? reject(err) : resolve()) - }) - )) - }) - .catch(() => { throw error(500, "Can't list container") }) - .then(() => new Promise((resolve, reject) => { - // TODO 'text/turtle' is fixed, should be contentType instead - // This forces one more translation turtle -> desired - serialize(resourceGraph, reqUri, 'text/turtle', function (err, result) { - if (err) { - debug.handlers('GET -- Error serializing container: ' + err) - reject(error(500, "Can't serialize container")) - } else { - resolve(result) - } - }) - })) - .then(result => callback(null, result), callback) + throw error(500, "Can't parse container") + } + + try { + // add container stats + await ldpContainer.addContainerStats(this, reqUri, container, resourceGraph) + // read directory + const files = await ldpContainer.readdir(container) + // iterate through all the files + await Promise.all(files.map(async file => { + const { url: fileUri } = await this.resourceMapper.mapFileToUrl( + { path: join(container, file), hostname }) + return await ldpContainer.addFile(this, resourceGraph, reqUri, fileUri, container, file) + })) + } catch (err) { + throw error(500, "Can't list container") + } + + // TODO 'text/turtle' is fixed, should be contentType instead + // This forces one more translation turtle -> desired + try { + return await serialize(resourceGraph, reqUri, 'text/turtle') + } catch (err) { + debug.handlers('GET -- Error serializing container: ' + err) + throw error(500, "Can't serialize container") + } } - post (host, containerPath, stream, { container, slug, extension }, callback) { + async post (host, containerPath, stream, { container, slug, extension }) { const ldp = this debug.handlers('POST -- On parent: ' + containerPath) // prepare slug if (slug) { slug = decodeURIComponent(slug) if (slug.match(/\/|\||:/)) { - callback(error(400, 'The name of new file POSTed may not contain : | or /')) - return + throw error(400, 'The name of new file POSTed may not contain : | or /') } } // Containers should not receive an extension @@ -203,21 +148,20 @@ class LDP { extension = '' } // TODO: possibly package this in ldp.post - ldp.getAvailablePath(host, containerPath, { slug, extension }).then(resourcePath => { - debug.handlers('POST -- Will create at: ' + resourcePath) - let originalPath = resourcePath - if (container) { - // Create directory by an LDP PUT to the container's .meta resource - resourcePath = path.join(originalPath, ldp.suffixMeta) - if (originalPath && !originalPath.endsWith('/')) { - originalPath += '/' - } + let resourcePath = await ldp.getAvailablePath(host, containerPath, { slug, extension }) + debug.handlers('POST -- Will create at: ' + resourcePath) + let originalPath = resourcePath + if (container) { + // Create directory by an LDP PUT to the container's .meta resource + resourcePath = join(originalPath, ldp.suffixMeta) + if (originalPath && !originalPath.endsWith('/')) { + originalPath += '/' } - ldp.put(host, resourcePath, stream, function (err) { - if (err) callback(err) - callback(null, originalPath) - }) - }) + } + const { url: putUrl, contentType } = await this.resourceMapper.mapFileToUrl( + { path: this.resourceMapper.rootPath + resourcePath, hostname: host }) + await ldp.put(putUrl, stream, contentType) + return originalPath } /** @@ -238,74 +182,83 @@ class LDP { * * @return {Promise} */ - putGraph (graph, uri, contentType = DEFAULT_CONTENT_TYPE) { - return new Promise((resolve, reject) => { - let parsedUri = url.parse(uri) - let hostname = parsedUri.hostname - let path = parsedUri.pathname - - serialize(graph, uri, contentType, (error, content) => { - if (error) { return reject(error) } - - let stream = stringToStream(content) - - this.put(hostname, path, stream, (error) => { - if (error) { return reject(error) } - - resolve(graph) - }) - }) - }) + async putGraph (graph, uri, contentType) { + const { path } = url.parse(uri) + const content = await serialize(graph, uri, contentType) + let stream = stringToStream(content) + return await this.put(path, stream, contentType) } - put (host, resourcePath, stream, callback) { - const ldp = this - const root = !ldp.multiuser ? ldp.root : ldp.root + host + '/' - const filePath = utils.uriToFilename(resourcePath, root, host) + isValidRdf (body, requestUri, contentType) { + const resourceGraph = $rdf.graph() + try { + $rdf.parse(body, resourceGraph, requestUri, contentType) + } catch (err) { + debug.ldp('VALIDATE -- Error parsing data: ' + err) + return false + } + return true + } + async put (url, stream, contentType) { // PUT requests not supported on containers. Use POST instead - if (resourcePath.endsWith('/')) { - return callback(error(409, - 'PUT not supported on containers, use POST instead')) + if ((url.url || url).endsWith('/')) { + throw error(409, + 'PUT not supported on containers, use POST instead') + } + + // PUT without content type is forbidden + if (!contentType) { + throw error(415, + 'PUT request require a valid content type via the Content-Type header') } // First check if we are above quota - overQuota(root, this.serverUri).then((isOverQuota) => { - if (isOverQuota) { - return callback(error(413, - 'User has exceeded their storage quota')) - } + let isOverQuota + try { + isOverQuota = await overQuota(this.resourceMapper.rootPath, this.serverUri) + } catch (err) { + throw error(500, 'Error finding user quota') + } + if (isOverQuota) { + throw error(413, 'User has exceeded their storage quota') + } - // Second, create the enclosing directory, if necessary - const dirName = path.dirname(filePath) - mkdirp(dirName, (err) => { - if (err) { - debug.handlers('PUT -- Error creating directory: ' + err) - return callback(error(err, - 'Failed to create the path to the new resource')) - } - // Directory created, now write the file - const file = stream.pipe(fs.createWriteStream(filePath)) + // Second, create the enclosing directory, if necessary + const { path } = await this.resourceMapper.mapUrlToFile({ url, contentType, createIfNotExists: true }) + const dirName = dirname(path) + try { + await promisify(mkdirp)(dirName) + } catch (err) { + debug.handlers('PUT -- Error creating directory: ' + err) + throw error(err, + 'Failed to create the path to the new resource') + } + + // Directory created, now write the file + let releaseLock + try { + releaseLock = await lock(path, { retries: 10, realpath: false }) + return await new Promise((resolve, reject) => { + const file = stream.pipe(fs.createWriteStream(path)) file.on('error', function () { - callback(error(500, 'Error writing data')) + reject(error(500, 'Error writing data')) }) file.on('finish', function () { - debug.handlers('PUT -- Wrote data to: ' + filePath) - callback(null) + debug.handlers('PUT -- Wrote data to: ' + path) + resolve() }) }) - }).catch(() => callback(error(500, 'Error finding user quota'))) + } finally { + if (releaseLock) { + await releaseLock() + } + } } - exists (hostname, path, callback) { - const options = { hostname, path, includeBody: false } - if (callback) { - return this.get(options, callback) - } else { - return new Promise((resolve, reject) => { - this.get(options, err => err ? reject(err) : resolve(true)) - }) - } + async exists (hostname, path, searchIndex = true) { + const options = { hostname, path, includeBody: false, searchIndex } + return await this.get(options, searchIndex) } /** @@ -325,7 +278,7 @@ class LDP { * * @return {Promise} */ - async fetchGraph (uri, options) { + async fetchGraph (uri, options = {}) { const response = await fetch(uri) if (!response.ok) { const error = new Error( @@ -336,9 +289,7 @@ class LDP { } const body = await response.text() - const contentType = options.contentType || DEFAULT_CONTENT_TYPE - - return promisify(parse)(body, uri, contentType) + return parse(body, uri, getContentType(response.headers)) } /** @@ -359,201 +310,176 @@ class LDP { * * @return {Promise} */ - getGraph (uri, contentType = DEFAULT_CONTENT_TYPE) { - const parsedUri = url.parse(uri) - const path = parsedUri.pathname - const hostname = parsedUri.hostname - - return new Promise((resolve, reject) => { - this.graph(hostname, path, uri, contentType, (error, graph) => { - if (error) { return reject(error) } - - resolve(graph) - }) - }) + getGraph (uri, contentType) { + return this.graph(uri, uri, contentType) } - graph (host, reqPath, baseUri, contentType, callback) { - const ldp = this - - // overloading - if (typeof contentType === 'function') { - callback = contentType - contentType = 'text/turtle' + async graph (url, baseUri, contentType) { + const body = await this.readResource(url) + if (!contentType) { + ({ contentType } = await this.resourceMapper.mapUrlToFile({ url })) } - - if (typeof baseUri === 'function') { - callback = baseUri - baseUri = undefined - } - - const root = ldp.multiuser ? ldp.root + host + '/' : ldp.root - const filename = utils.uriToFilename(reqPath, root) - - ldp.readFile(filename, (err, body) => { - if (err) return callback(err) - parse(body, baseUri, contentType, callback) + return new Promise((resolve, reject) => { + const graph = $rdf.graph() + $rdf.parse(body, graph, baseUri, contentType, + err => err ? reject(err) : resolve(graph)) }) } - get (options, callback) { - let host - let reqPath - let baseUri - let includeBody - let contentType - let range - if (options) { - host = options.hostname - reqPath = options.path - baseUri = options.baseUri - includeBody = options.includeBody - contentType = options.possibleRDFType - range = options.range + async get (options, searchIndex = true) { + let path, contentType, stats + try { + ({ path, contentType } = await this.resourceMapper.mapUrlToFile({ url: options, searchIndex })) + stats = await this.stat(path) + } catch (err) { + throw error(404, 'Can\'t find file requested: ' + options) } - const ldp = this - const root = !ldp.multiuser ? ldp.root : ldp.root + host + '/' - const filename = utils.uriToFilename(reqPath, root) - ldp.stat(filename, function (err, stats) { - // File does not exist - if (err) { - return callback(error(err, 'Can\'t find file requested: ' + filename)) - } + // Just return, since resource exists + if (!options.includeBody) { + return { stream: stats, contentType, container: stats.isDirectory() } + } - // windows does not differentiate between file paths with or without trailing slash - if (reqPath.substr(-1) === '/' && !stats.isDirectory()) { - return callback(error(404, 'Can\'t find directory requested: ' + filename)) + // Found a container + if (stats.isDirectory()) { + const { url: absContainerUri } = await this.resourceMapper + .mapFileToUrl({ path, hostname: options.hostname }) + const metaFile = await this.readContainerMeta(absContainerUri) + .catch(() => '') // Default to an empty meta file if it is missing + let data + try { + data = await this.listContainer(path, absContainerUri, metaFile, options.hostname) + } catch (err) { + debug.handlers('GET container -- Read error:' + err.message) + throw err } - - // Just return, since resource exists - if (!includeBody) { - return callback(null, {'stream': stats, 'contentType': contentType, 'container': stats.isDirectory()}) + const stream = stringToStream(data) + // TODO 'text/turtle' is fixed, should be contentType instead + // This forces one more translation turtle -> desired + return { stream, contentType: 'text/turtle', container: true } + } else { + let chunksize, contentRange, start, end + if (options.range) { + const total = fs.statSync(path).size + const parts = options.range.replace(/bytes=/, '').split('-') + const partialstart = parts[0] + const partialend = parts[1] + start = parseInt(partialstart, 10) + end = partialend ? parseInt(partialend, 10) : total - 1 + chunksize = (end - start) + 1 + contentRange = 'bytes ' + start + '-' + end + '/' + total } - - // Found a container - if (stats.isDirectory()) { - return ldp.readContainerMeta(filename, function (err, metaFile) { - if (err) { - metaFile = '' - } - const absContainerUri = baseUri + reqPath - ldp.listContainer(filename, absContainerUri, baseUri, metaFile, contentType, - function (err, data) { - if (err) { - debug.handlers('GET container -- Read error:' + err.message) - return callback(err) - } - const stream = stringToStream(data) - // TODO 'text/turtle' is fixed, should be contentType instead - // This forces one more translation turtle -> desired - return callback(null, {'stream': stream, 'contentType': 'text/turtle', 'container': true}) + let releaseLock + try { + releaseLock = await lock(path, { retries: 10 }) + return await new Promise((resolve, reject) => { + const stream = fs.createReadStream(path, start && end ? {start, end} : {}) + stream + .on('error', function (err) { + debug.handlers(`GET -- error reading ${path}: ${err.message}`) + return reject(error(err, "Can't read file " + err)) + }) + .on('open', function () { + debug.handlers(`GET -- Reading ${path}`) + return resolve({ stream, contentType, container: false, contentRange, chunksize }) }) }) - } else { - let stream - let chunksize - let contentRange - if (range) { - const total = fs.statSync(filename).size - const parts = range.replace(/bytes=/, '').split('-') - const partialstart = parts[0] - const partialend = parts[1] - const start = parseInt(partialstart, 10) - const end = partialend ? parseInt(partialend, 10) : total - 1 - chunksize = (end - start) + 1 - contentRange = 'bytes ' + start + '-' + end + '/' + total - stream = ldp.createReadStream(filename, start, end) - } else { - stream = ldp.createReadStream(filename) + } finally { + if (releaseLock) { + await releaseLock() } - stream - .on('error', function (err) { - debug.handlers(`GET -- error reading ${filename}: ${err.message}`) - return callback(error(err, "Can't read file " + err)) - }) - .on('open', function () { - debug.handlers(`GET -- Reading ${filename}`) - let contentType = mime.lookup(filename) || DEFAULT_CONTENT_TYPE - if (utils.hasSuffix(filename, ldp.turtleExtensions)) { - contentType = 'text/turtle' - } - return callback(null, {'stream': stream, 'contentType': contentType, 'container': false, 'contentRange': contentRange, 'chunksize': chunksize}) - }) } - }) + } } - delete (host, resourcePath, callback) { - const ldp = this - const root = !ldp.multiuser ? ldp.root : ldp.root + host + '/' - const filename = utils.uriToFilename(resourcePath, root) - ldp.stat(filename, function (err, stats) { - if (err) { - return callback(error(404, "Can't find " + err)) - } + async delete (url) { + // First check if the path points to a valid file + let path, stats + try { + ({ path } = await this.resourceMapper.mapUrlToFile({ url })) + stats = await this.stat(path) + } catch (err) { + throw error(404, "Can't find " + err) + } - if (stats.isDirectory()) { - return ldp.deleteContainer(filename, callback) - } else { - return ldp.deleteResource(filename, callback) - } - }) + // If so, delete the directory or file + if (stats.isDirectory()) { + return this.deleteContainer(path) + } else { + return this.deleteResource(path) + } } - deleteContainer (directory, callback) { - const self = this + async deleteContainer (directory) { if (directory[ directory.length - 1 ] !== '/') { directory += '/' } - let countValid = 0 - fs.readdir(directory, function (err, list) { - if (err) return callback(error(404, 'The container does not exist')) - - if (list.indexOf(self.suffixMeta) > -1) { - countValid++ - } - - if (list.indexOf(self.suffixAcl) > -1) { - countValid++ - } + // Ensure the container exists + let list + try { + list = await promisify(fs.readdir)(directory) + } catch (err) { + throw error(404, 'The container does not exist') + } - if (list.length !== countValid) { - return callback(error(409, 'Container is not empty')) - } + // Ensure the container is empty (we ignore .meta and .acl) + if (list.some(file => file !== this.suffixMeta && file !== this.suffixAcl)) { + throw error(409, 'Container is not empty') + } - return rimraf(directory, function (err) { - if (err) return callback(error(err, 'Failed to delete the container')) - return callback(null) - }) - }) + // Delete the directory recursively + try { + await promisify(rimraf)(directory) + } catch (err) { + throw error(err, 'Failed to delete the container') + } } - deleteResource (filename, callback) { - return fs.unlink(filename, function (err, data) { - if (err) { - debug.container('DELETE -- unlink() error: ' + err) - return callback(error(err, 'Failed to delete resource')) + async deleteResource (path) { + let releaseLock + try { + releaseLock = await lock(path, { retries: 10 }) + return await promisify(fs.unlink)(path) + } catch (err) { + debug.container('DELETE -- unlink() error: ' + err) + throw error(err, 'Failed to delete resource') + } finally { + if (releaseLock) { + await releaseLock() } - return callback(null, data) - }) + } } getAvailablePath (host, containerURI, { slug = uuid.v1(), extension }) { - const filename = slug + extension + const path = slug + extension function ensureNotExists (self, newPath) { // Verify whether the new path already exists return self.exists(host, newPath).then( // If it does, generate another one () => ensureNotExists(self, URI.joinPaths(containerURI, - `${uuid.v1().split('-')[0]}-${filename}`).toString()), + `${uuid.v1().split('-')[0]}-${path}`).toString()), // If not, we found an appropriate path () => newPath ) } - return ensureNotExists(this, URI.joinPaths(containerURI, filename).toString()) + return ensureNotExists(this, URI.joinPaths(containerURI, path).toString()) + } + + getTrustedOrigins (req) { + let trustedOrigins = [this.resourceMapper.resolveUrl(req.hostname)].concat(this.trustedOrigins) + if (this.multiuser) { + trustedOrigins.push(this.serverUri) + } + return trustedOrigins + } + + static mimeTypeIsRdf (mimeType) { + return RDF_MIME_TYPES.has(mimeType) + } + + static mimeTypesAsArray () { + return Array.from(RDF_MIME_TYPES) } } module.exports = LDP -module.exports.RDF_MIME_TYPES = RDF_MIME_TYPES diff --git a/lib/legacy-resource-mapper.js b/lib/legacy-resource-mapper.js deleted file mode 100644 index de9d44a2b..000000000 --- a/lib/legacy-resource-mapper.js +++ /dev/null @@ -1,21 +0,0 @@ -const ResourceMapper = require('./resource-mapper') - -// A LegacyResourceMapper models the old mapping between HTTP URLs and server filenames, -// and is intended to be replaced by ResourceMapper -class LegacyResourceMapper extends ResourceMapper { - constructor (options) { - super(Object.assign({ defaultContentType: 'text/turtle' }, options)) - } - - // Maps the request for a given resource and representation format to a server file - async mapUrlToFile ({ url }) { - return { path: this._getFullPath(url), contentType: this._getContentTypeByExtension(url) } - } - - // Preserve dollars in paths - _removeDollarExtension (path) { - return path - } -} - -module.exports = LegacyResourceMapper diff --git a/lib/models/account-manager.js b/lib/models/account-manager.js index cb0c70359..31aa84ca6 100644 --- a/lib/models/account-manager.js +++ b/lib/models/account-manager.js @@ -1,7 +1,6 @@ 'use strict' const url = require('url') -const path = require('path') const rdf = require('rdflib') const ns = require('solid-namespace')(rdf) @@ -84,18 +83,18 @@ class AccountManager { */ accountExists (accountName) { let accountUri - let rootAclPath + let cardPath try { accountUri = this.accountUriFor(accountName) accountUri = url.parse(accountUri).hostname - rootAclPath = url.resolve('/', this.store.suffixAcl) + cardPath = url.resolve('/', this.pathCard) } catch (err) { return Promise.reject(err) } - return this.accountUriExists(accountUri, rootAclPath) + return this.accountUriExists(accountUri, cardPath) } /** @@ -107,16 +106,12 @@ class AccountManager { * * @return {Promise} */ - accountUriExists (accountUri, accountResource = '/') { - return new Promise((resolve, reject) => { - this.store.exists(accountUri, accountResource, (err, result) => { - if (err && err.status === 404) { - return resolve(false) - } - - resolve(!!result) - }) - }) + async accountUriExists (accountUri, accountResource = '/') { + try { + return await this.store.exists(accountUri, accountResource) + } catch (err) { + return false + } } /** @@ -135,17 +130,7 @@ class AccountManager { * @return {string} */ accountDirFor (accountName) { - let accountDir - - if (this.multiuser) { - let uri = this.accountUriFor(accountName) - let hostname = url.parse(uri).hostname - accountDir = path.join(this.store.root, hostname) - } else { - // single user mode - accountDir = this.store.root - } - return accountDir + return this.store.resourceMapper.getBasePath(url.parse(this.accountUriFor(accountName)).hostname) } /** diff --git a/lib/models/account-template.js b/lib/models/account-template.js index dfedc916e..a645271b6 100644 --- a/lib/models/account-template.js +++ b/lib/models/account-template.js @@ -5,8 +5,8 @@ const mime = require('mime-types') const recursiveRead = require('recursive-readdir') const fsUtils = require('../common/fs-utils') const templateUtils = require('../common/template-utils') +const LDP = require('../ldp') -const RDF_MIME_TYPES = require('../ldp').RDF_MIME_TYPES const TEMPLATE_EXTENSIONS = [ '.acl', '.meta', '.json', '.hbs', '.handlebars' ] const TEMPLATE_FILES = [ 'card' ] @@ -31,7 +31,6 @@ class AccountTemplate { */ constructor (options = {}) { this.substitutions = options.substitutions || {} - this.rdfMimeTypes = options.rdfMimeTypes || RDF_MIME_TYPES this.templateExtensions = options.templateExtensions || TEMPLATE_EXTENSIONS this.templateFiles = options.templateFiles || TEMPLATE_FILES } @@ -136,11 +135,12 @@ class AccountTemplate { * @return {boolean} */ isTemplate (filePath) { - let parsed = path.parse(filePath) + const parsed = path.parse(filePath) - let isRdf = this.rdfMimeTypes.includes(mime.lookup(filePath)) - let isTemplateExtension = this.templateExtensions.includes(parsed.ext) - let isTemplateFile = this.templateFiles.includes(parsed.base) || + const mimeType = mime.lookup(filePath) + const isRdf = LDP.mimeTypeIsRdf(mimeType) + const isTemplateExtension = this.templateExtensions.includes(parsed.ext) + const isTemplateFile = this.templateFiles.includes(parsed.base) || this.templateExtensions.includes(parsed.base) // the '/.acl' case return isRdf || isTemplateExtension || isTemplateFile diff --git a/lib/models/solid-host.js b/lib/models/solid-host.js index 663546f62..e13f156ca 100644 --- a/lib/models/solid-host.js +++ b/lib/models/solid-host.js @@ -15,7 +15,12 @@ class SolidHost { * @param [options={}] * @param [options.port] {number} * @param [options.serverUri] {string} Fully qualified URI that this Solid - * server is listening on, e.g. `https://databox.me` + * server is listening on, e.g. `https://solid.community` + * @param [options.live] {boolean} Whether to turn on WebSockets / LDP live + * @param [options.root] {string} Path to root data directory + * @param [options.multiuser] {boolean} Multiuser mode + * @param [options.webid] {boolean} Enable WebID-related functionality + * (account creation and authentication) */ constructor (options = {}) { this.port = options.port || defaults.port @@ -24,6 +29,10 @@ class SolidHost { this.parsedUri = url.parse(this.serverUri) this.host = this.parsedUri.host this.hostname = this.parsedUri.hostname + this.live = options.live + this.root = options.root + this.multiuser = options.multiuser + this.webid = options.webid } /** @@ -69,7 +78,7 @@ class SolidHost { * @param origin {?string} * @return {boolean} */ - allowsSessionFor (userId, origin) { + allowsSessionFor (userId, origin, trustedOrigins) { // Allow no user or an empty origin if (!userId || !origin) return true // Allow the server and subdomains @@ -80,6 +89,7 @@ class SolidHost { // Allow the user's own domain const userHost = getHostName(userId) if (originHost === userHost) return true + if (trustedOrigins.includes(origin)) return true return false } diff --git a/lib/requests/create-account-request.js b/lib/requests/create-account-request.js index a130e7139..ee6971043 100644 --- a/lib/requests/create-account-request.js +++ b/lib/requests/create-account-request.js @@ -38,6 +38,7 @@ class CreateAccountRequest extends AuthRequest { this.username = options.username this.userAccount = options.userAccount this.acceptToc = options.acceptToc + this.disablePasswordChecks = options.disablePasswordChecks } /** @@ -69,6 +70,7 @@ class CreateAccountRequest extends AuthRequest { options.enforceToc = locals.enforceToc options.tocUri = locals.tocUri + options.disablePasswordChecks = locals.disablePasswordChecks switch (authMethod) { case 'oidc': @@ -113,7 +115,8 @@ class CreateAccountRequest extends AuthRequest { multiuser: this.accountManager.multiuser, registerDisabled: authMethod === 'tls', returnToUrl: this.returnToUrl, - tocUri: this.tocUri + tocUri: this.tocUri, + disablePasswordChecks: this.disablePasswordChecks }) if (error) { @@ -307,8 +310,7 @@ class CreateOidcAccountRequest extends CreateAccountRequest { } sendResponse (userAccount) { - let redirectUrl = this.returnToUrl || - this.accountManager.accountUriFor(userAccount.username) + let redirectUrl = this.returnToUrl || this.loginUrl() this.response.redirect(redirectUrl) return userAccount diff --git a/lib/resource-mapper.js b/lib/resource-mapper.js index 47708dc8a..2abeeb1dd 100644 --- a/lib/resource-mapper.js +++ b/lib/resource-mapper.js @@ -3,17 +3,30 @@ const URL = require('url') const { promisify } = require('util') const { types, extensions } = require('mime-types') const readdir = promisify(fs.readdir) +const HTTPError = require('./http-error') // A ResourceMapper maintains the mapping between HTTP URLs and server filenames, // following the principles of the “sweet spot” discussed in // https://www.w3.org/DesignIssues/HTTPFilenameMapping.html class ResourceMapper { - constructor ({ rootUrl, rootPath, includeHost, defaultContentType = 'application/octet-stream' }) { + constructor ({ + rootUrl, + rootPath, + includeHost = false, + defaultContentType = 'application/octet-stream', + indexFilename = 'index.html', + overrideTypes = { acl: 'text/turtle', meta: 'text/turtle' }, + fileSuffixes = ['.acl', '.meta'] + }) { this._rootUrl = this._removeTrailingSlash(rootUrl) this._rootPath = this._removeTrailingSlash(rootPath) this._includeHost = includeHost this._readdir = readdir this._defaultContentType = defaultContentType + this._types = { ...types, ...overrideTypes } + this._indexFilename = indexFilename + this._indexContentType = this._getContentTypeByExtension(indexFilename) + this._isControlFile = new RegExp(`(?:${fileSuffixes.map(fs => fs.replace('.', '\\.')).join('|')})$`) // If the host needs to be replaced on every call, pre-split the root URL if (includeHost) { @@ -24,29 +37,49 @@ class ResourceMapper { } } + get rootPath () { + return this._rootPath + } + // Maps the request for a given resource and representation format to a server file - async mapUrlToFile ({ url, contentType, createIfNotExists }) { - const fullPath = this._getFullPath(url) + // When searchIndex is true and the URL ends with a '/', indexFilename will be matched. + async mapUrlToFile ({ url, contentType, createIfNotExists, searchIndex = true }) { + let fullPath = this.getFullPath(url) + let isIndex = searchIndex && fullPath.endsWith('/') let path + // Append index filename if the URL ends with a '/' + if (isIndex) { + if (createIfNotExists && contentType !== this._indexContentType) { + throw new Error(`Index file needs to have ${this._indexContentType} as content type`) + } + fullPath += this._indexFilename + } + // Create the path for a new file if (createIfNotExists) { path = fullPath // If the extension is not correct for the content type, append the correct extension - if (this._getContentTypeByExtension(path) !== contentType) { - path += contentType in extensions ? `$.${extensions[contentType][0]}` : '$.unknown' + if (searchIndex && this._getContentTypeByExtension(path) !== contentType) { + path += `$${contentType in extensions ? `.${extensions[contentType][0]}` : '.unknown'}` } // Determine the path of an existing file } else { // Read all files in the corresponding folder const filename = fullPath.substr(fullPath.lastIndexOf('/') + 1) const folder = fullPath.substr(0, fullPath.length - filename.length) - const files = await this._readdir(folder) // Find a file with the same name (minus the dollar extension) - const match = files.find(f => this._removeDollarExtension(f) === filename) - if (!match) { - throw new Error('File not found') + let match = searchIndex ? await this._getMatchingFile(folder, filename, isIndex) : '' + if (match === undefined) { + // Error if no match was found, + // unless the URL ends with a '/', + // in that case we fallback to the folder itself. + if (isIndex) { + match = '' + } else { + throw new HTTPError(404, `File not found: ${fullPath}`) + } } path = `${folder}${match}` contentType = this._getContentTypeByExtension(match) @@ -55,12 +88,42 @@ class ResourceMapper { return { path, contentType: contentType || this._defaultContentType } } + async _getMatchingFile (folder, filename, isIndex) { + const files = await this._readdir(folder) + // Search for files with the same name (disregarding a dollar extension) + if (!isIndex) { + return files.find(f => this._removeDollarExtension(f) === filename) + // Check if the index file exists + } else if (files.includes(this._indexFilename)) { + return this._indexFilename + } + } + + async getRepresentationUrlForResource (resourceUrl) { + let fullPath = this.getFullPath(resourceUrl) + let isIndex = fullPath.endsWith('/') + + // Append index filename if the URL ends with a '/' + if (isIndex) { + fullPath += this._indexFilename + } + + // Read all files in the corresponding folder + const filename = fullPath.substr(fullPath.lastIndexOf('/') + 1) + const folder = fullPath.substr(0, fullPath.length - filename.length) + const files = await this._readdir(folder) + + // Find a file with the same name (minus the dollar extension) + let match = (files.find(f => this._removeDollarExtension(f) === filename || (isIndex && f.startsWith(this._indexFilename + '.')))) + return `${resourceUrl}${match || ''}` + } + // Maps a given server file to a URL async mapFileToUrl ({ path, hostname }) { // Determine the URL by chopping off everything after the dollar sign let pathname = this._removeDollarExtension(path.substring(this._rootPath.length)) pathname = this._replaceBackslashes(pathname) - const url = `${this.getBaseUrl(hostname)}${encodeURI(pathname)}` + const url = `${this.resolveUrl(hostname)}${encodeURI(pathname)}` return { url, contentType: this._getContentTypeByExtension(path) } } @@ -69,15 +132,16 @@ class ResourceMapper { return !this._includeHost ? this._rootPath : `${this._rootPath}/${hostname}` } - // Gets the base URL for the given hostname - getBaseUrl (hostname) { - return !this._includeHost ? this._rootUrl - : `${this._protocol}//${hostname}${this._port}${this._rootUrl}` + // Resolve a URL for the given hostname + // Optionally, a pathname may be passed that will be appended to the baseUrl. + resolveUrl (hostname, pathname = '') { + return !this._includeHost ? `${this._rootUrl}${pathname}` + : `${this._protocol}//${hostname}${this._port}${this._rootUrl}${pathname}` } // Determine the full file path corresponding to a URL - _getFullPath (url) { - const { pathname, hostname } = typeof url === 'string' ? URL.parse(url) : url + getFullPath (url) { + const { pathname, hostname } = this._parseUrl(url) const fullPath = decodeURIComponent(`${this.getBasePath(hostname)}${pathname}`) if (fullPath.indexOf('/..') >= 0) { throw new Error('Disallowed /.. segment in URL') @@ -85,10 +149,25 @@ class ResourceMapper { return fullPath } + // Parses a URL into a hostname and pathname + _parseUrl (url) { + // URL specified as string + if (typeof url === 'string') { + return URL.parse(url) + } + // URL specified as Express request object + if (!url.pathname && url.path) { + const { hostname, path } = url + return { hostname, pathname: path.replace(/[?#].*/, '') } + } + // URL specified as object + return url + } + // Gets the expected content type based on the extension of the path _getContentTypeByExtension (path) { const extension = /\.([^/.]+)$/.exec(path) - return extension && types[extension[1].toLowerCase()] || this._defaultContentType + return extension && this._types[extension[1].toLowerCase()] || this._defaultContentType } // Removes a possible trailing slash from a path diff --git a/lib/server-config.js b/lib/server-config.js index 78df260f2..cb49386f9 100644 --- a/lib/server-config.js +++ b/lib/server-config.js @@ -9,6 +9,25 @@ const path = require('path') const templateUtils = require('./common/template-utils') const fsUtils = require('./common/fs-utils') +const debug = require('./debug') + +function printDebugInfo (options) { + debug.settings('Server URI: ' + options.serverUri) + debug.settings('Auth method: ' + options.auth) + debug.settings('Strict origins: ' + options.strictOrigin) + debug.settings('Allowed origins: ' + options.trustedOrigins) + debug.settings('Db path: ' + options.dbPath) + debug.settings('Config path: ' + options.configPath) + debug.settings('Suffix Acl: ' + options.suffixAcl) + debug.settings('Suffix Meta: ' + options.suffixMeta) + debug.settings('Filesystem Root: ' + (this.resourceMapper ? this.resourceMapper.rootPath : 'none')) + debug.settings('Allow WebID authentication: ' + !!options.webid) + debug.settings('Live-updates: ' + !!options.live) + debug.settings('Multi-user: ' + !!options.multiuser) + debug.settings('Suppress default data browser app: ' + options.suppressDataBrowser) + debug.settings('Default data browser app file path: ' + options.dataBrowserPath) +} + /** * Ensures that a directory has been copied / initialized. Used to ensure that * account templates, email templates and default apps have been copied from @@ -40,9 +59,8 @@ function ensureDirCopyExists (fromDir, toDir) { * @param argv {Object} Express.js app object */ async function ensureWelcomePage (argv) { - const { multiuser, templates, server, host } = argv - const rootDir = path.resolve(argv.root) - const serverRootDir = multiuser ? path.join(rootDir, argv.host.hostname) : rootDir + const { resourceMapper, templates, server, host } = argv + const serverRootDir = resourceMapper.getBasePath(host.hostname) const existingIndexPage = path.join(serverRootDir, 'index.html') const packageData = require('../package.json') @@ -56,6 +74,13 @@ async function ensureWelcomePage (argv) { serverVersion: packageData.version }) } + + // Ensure that the root .acl file exists, + // because this was not mandatory in before 5.0.0 + const existingRootAcl = path.join(serverRootDir, '.acl') + if (!fs.existsSync(existingRootAcl)) { + await fsUtils.copyTemplateDir(path.join(templates.server, '.acl'), existingRootAcl) + } } /** @@ -119,6 +144,13 @@ function initTemplateDirs (configPath) { path.join(configPath, 'templates', 'server') ) + // Ensure that the root .acl file exists, + // because this was not mandatory in before 5.0.0 + ensureDirCopyExists( + path.join(__dirname, '../default-templates/server/.acl'), + path.join(configPath, 'templates', 'server', '.acl') + ) + return { account: accountTemplatePath, email: emailTemplatesPath, @@ -131,5 +163,6 @@ module.exports = { ensureWelcomePage, initConfigDir, initDefaultViews, - initTemplateDirs + initTemplateDirs, + printDebugInfo } diff --git a/lib/utils.js b/lib/utils.js index 6a29cbd00..21e5f6870 100644 --- a/lib/utils.js +++ b/lib/utils.js @@ -1,22 +1,18 @@ -module.exports.uriToFilename = uriToFilename -module.exports.uriToRelativeFilename = uriToRelativeFilename -module.exports.getBaseUri = getBaseUri module.exports.pathBasename = pathBasename -module.exports.getFullUri = getFullUri module.exports.hasSuffix = hasSuffix -module.exports.parse = parse module.exports.serialize = serialize module.exports.translate = translate module.exports.stringToStream = stringToStream -module.exports.reqToPath = reqToPath module.exports.debrack = debrack module.exports.stripLineEndings = stripLineEndings module.exports.fullUrlForReq = fullUrlForReq module.exports.routeResolvedFile = routeResolvedFile module.exports.getQuota = getQuota module.exports.overQuota = overQuota +module.exports.getContentType = getContentType +module.exports.parse = parse -const fs = require('fs-extra') +const fs = require('fs') const path = require('path') const util = require('util') const $rdf = require('rdflib') @@ -74,55 +70,20 @@ function debrack (s) { return s.substring(1, s.length - 1) } -function uriToFilename (uri, base) { - const decoded = uri.split('/').map(decodeURIComponent).join('/') - let filename = path.join(base, decoded) - // Make sure filename ends with '/' if filename exists and is a directory. - // TODO this sync operation can be avoided and can be left - // to do, to other components, see `ldp.get` - try { - const fileStats = fs.statSync(filename) - if (fileStats.isDirectory() && !filename.endsWith('/')) { - filename += '/' - } else if (fileStats.isFile() && filename.endsWith('/')) { - filename = filename.substr(0, filename.length - 1) +async function parse (data, baseUri, contentType) { + const graph = $rdf.graph() + return new Promise((resolve, reject) => { + try { + return $rdf.parse(data, graph, baseUri, contentType, (err, str) => { + if (err) { + return reject(err) + } + resolve(str) + }) + } catch (err) { + return reject(err) } - } catch (err) {} - return filename -} - -function uriToRelativeFilename (uri, base) { - const filename = uriToFilename(uri, base) - const relative = path.relative(base, filename) - return relative -} - -function getBaseUri (req) { - // Obtain the protocol from the configured server URI - // (in case the server is running behind a reverse proxy) - const locals = req.app.locals - const serverUri = locals.host.serverUri - const protocol = serverUri.replace(/:.*/, '') - - return `${protocol || req.protocol}://${req.get('host')}` -} - -/** - * Composes and returns the fully-qualified URI for the request, to be used - * as a base URI for RDF parsing or serialization. For example, if a request - * is to `Host: example.com`, `GET /files/` using the `https:` protocol, - * then: - * - * ``` - * getFullUri(req) // -> 'https://example.com/files/' - * ``` - * - * @param req {IncomingMessage} - * - * @return {string} - */ -function getFullUri (req) { - return getBaseUri(req) + url.resolve(req.baseUrl, req.path) + }) } function pathBasename (fullpath) { @@ -144,59 +105,42 @@ function hasSuffix (path, suffixes) { return false } -function parse (data, baseUri, contentType, callback) { - const graph = $rdf.graph() - try { - return $rdf.parse(data, graph, baseUri, contentType, callback) - } catch (err) { - return callback(err) - } -} - -function serialize (graph, baseUri, contentType, callback) { - try { - // target, kb, base, contentType, callback - $rdf.serialize(null, graph, baseUri, contentType, function (err, result) { - if (err) { - console.log(err) - return callback(err) - } - if (result === undefined) { - return callback(new Error('Error serializing the graph to ' + - contentType)) - } +function serialize (graph, baseUri, contentType) { + return new Promise((resolve, reject) => { + try { + // target, kb, base, contentType, callback + $rdf.serialize(null, graph, baseUri, contentType, function (err, result) { + if (err) { + return reject(err) + } + if (result === undefined) { + return reject(new Error('Error serializing the graph to ' + + contentType)) + } - return callback(null, result) - }) - } catch (err) { - console.log(err) - callback(err) - } + resolve(result) + }) + } catch (err) { + reject(err) + } + }) } -function translate (stream, baseUri, from, to, callback) { - // Handle Turtle Accept header - if (to === 'text/turtle' || - to === 'text/n3' || - to === 'application/turtle' || - to === 'application/n3') { - to = 'text/turtle' - } - - let data = '' - stream - .on('data', function (chunk) { - data += chunk - }) - .on('end', function () { - parse(data, baseUri, from, function (err, graph) { - if (err) return callback(err) - serialize(graph, baseUri, to, function (err, data) { - if (err) return callback(err) - callback(null, data) +function translate (stream, baseUri, from, to) { + return new Promise((resolve, reject) => { + let data = '' + stream + .on('data', function (chunk) { + data += chunk + }) + .on('end', function () { + const graph = $rdf.graph() + $rdf.parse(data, graph, baseUri, from, function (err) { + if (err) return reject(err) + resolve(serialize(graph, baseUri, to)) }) }) - }) + }) } function stringToStream (string) { @@ -231,12 +175,6 @@ function stripLineEndings (obj) { return obj.replace(/(\r\n|\n|\r)/gm, '') } -function reqToPath (req) { - const ldp = req.app.locals.ldp - const root = ldp.multiuser ? ldp.root + req.hostname + '/' : ldp.root - return uriToFilename(req.path, root) -} - /** * Adds a route that serves a static file from another Node module */ @@ -302,3 +240,20 @@ function actualSize (root) { function _asyncReadfile (filename) { return util.promisify(fs.readFile)(filename, 'utf-8') } + +/** + * Get the content type from a headers object + * @param headers An Express or Fetch API headers object + * @return {string} A content type string + */ +function getContentType (headers) { + const headerValue = headers.get ? headers.get('content-type') : headers['content-type'] + + // Default content type as stated by RFC 822 + if (!headerValue) { + return 'text/plain' + } + + // Remove charset suffix + return headerValue.split(';')[0] +} diff --git a/package-lock.json b/package-lock.json index bb58e972a..dfa6bd4b8 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,6 +1,6 @@ { "name": "solid-server", - "version": "4.4.2", + "version": "5.0.0-beta.11", "lockfileVersion": 1, "requires": true, "dependencies": { @@ -31,6 +31,12 @@ "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-2.5.2.tgz", "integrity": "sha512-OYu7XEzjkCQ3C5Ps3QIZsQfNpqoJyZZA99wd9aWd05NCtC5pWOkShK2mkL6HXQR6/Cy2lbNdPlZBpuQHXE63gA==", "dev": true + }, + "source-map": { + "version": "0.5.7", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.5.7.tgz", + "integrity": "sha1-igOdLRAh0i0eoUyA2OpGi6LvP8w=", + "dev": true } } }, @@ -213,6 +219,29 @@ "integrity": "sha512-nI7ELxukf7pT4/VraL4iabtNNMz8mUo7EXlqCFld8O5z6mIMLX9llps24iPpaIZOwArkY3FWA+4t+ixyvtTSIA==", "dev": true }, + "@solid/acl-check": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/@solid/acl-check/-/acl-check-0.2.0.tgz", + "integrity": "sha512-HjnwnpjvqE16k5yBNG0A7WIX9MveELv3Ge0DOJ8Cpt7e6aJ4sOpzY3A6x+ljOCOdxxT6CpGKhNGEdztZMGm1Qg==", + "requires": { + "rdflib": "^0.12.1", + "solid-namespace": "0.1.0" + }, + "dependencies": { + "rdflib": { + "version": "0.12.3", + "resolved": "http://registry.npmjs.org/rdflib/-/rdflib-0.12.3.tgz", + "integrity": "sha1-K0IGamYJwGtAqQSFhJNix8uV6h4=", + "requires": { + "async": "^0.9.x", + "jsonld": "^0.4.5", + "n3": "^0.4.1", + "xmldom": "^0.1.22", + "xmlhttprequest": "^1.7.0" + } + } + } + }, "@solid/better-simple-slideshow": { "version": "0.1.0", "resolved": "https://registry.npmjs.org/@solid/better-simple-slideshow/-/better-simple-slideshow-0.1.0.tgz", @@ -276,9 +305,9 @@ } }, "@solid/oidc-op": { - "version": "0.4.0", - "resolved": "https://registry.npmjs.org/@solid/oidc-op/-/oidc-op-0.4.0.tgz", - "integrity": "sha512-rOuFeeH58i86u3c2TfRMV39OH6ZUC4Q2B6fUqDMZVuyfsOpMicEq0Ok7XYWUlvOwYax3HUFA6NgNR05idSSG3g==", + "version": "0.4.2", + "resolved": "https://registry.npmjs.org/@solid/oidc-op/-/oidc-op-0.4.2.tgz", + "integrity": "sha512-GZ0GaMG0xgg8YiWM5LTkV9jDfa9dEljPpH3wJOHmsMRc29fLh0zcN6xrNQ0VlpEAVwVxLVE2CjhUOCyu0EqePw==", "requires": { "@solid/jose": "0.1.8", "@solid/keychain": "0.1.3", @@ -359,6 +388,11 @@ "text-encoding": "^0.6.1" } }, + "@types/node": { + "version": "10.12.9", + "resolved": "https://registry.npmjs.org/@types/node/-/node-10.12.9.tgz", + "integrity": "sha512-eajkMXG812/w3w4a1OcBlaTwsFPO5F7fJ/amy+tieQxEMWBlbV1JGSjkFM+zkHNf81Cad+dfIRA+IBkvmvdAeA==" + }, "@yarnpkg/lockfile": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/@yarnpkg/lockfile/-/lockfile-1.1.0.tgz", @@ -390,10 +424,14 @@ } }, "acorn": { - "version": "5.7.3", - "resolved": "https://registry.npmjs.org/acorn/-/acorn-5.7.3.tgz", - "integrity": "sha512-T/zvzYRfbVojPWahDsE5evJdHb3oJoQfFbsrKM7w5Zcs++Tr257tia3BmMP8XYVjp1S9RZXQMh7gao96BlqZOw==", - "dev": true + "version": "6.0.4", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-6.0.4.tgz", + "integrity": "sha512-VY4i5EKSKkofY2I+6QLTbTTN/UvEQPCo6eiwzzSaSWfpaDhOmStMCMod6wmuPciNq+XS0faCglFu2lHZpdHUtg==" + }, + "acorn-dynamic-import": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/acorn-dynamic-import/-/acorn-dynamic-import-4.0.0.tgz", + "integrity": "sha512-d3OEjQV4ROpoflsnUA8HozoIR504TFxNivYEUi6uwz0IYhBkTDXGuWlNdMtybRt3nqVx/L6XqMt0FxkXuWKZhw==" }, "acorn-jsx": { "version": "3.0.1", @@ -413,26 +451,20 @@ } }, "acorn-node": { - "version": "1.6.0", - "resolved": "https://registry.npmjs.org/acorn-node/-/acorn-node-1.6.0.tgz", - "integrity": "sha512-ZsysjEh+Y3i14f7YXCAKJy99RXbd56wHKYBzN4FlFtICIZyFpYwK6OwNJhcz8A/FMtxoUZkJofH1v9KIfNgWmw==", + "version": "1.6.2", + "resolved": "https://registry.npmjs.org/acorn-node/-/acorn-node-1.6.2.tgz", + "integrity": "sha512-rIhNEZuNI8ibQcL7ANm/mGyPukIaZsRNX9psFNQURyJW0nu6k8wjSDld20z6v2mDBWqX13pIEnk9gGZJHIlEXg==", "requires": { - "acorn": "^6.0.1", - "acorn-walk": "^6.0.1", + "acorn": "^6.0.2", + "acorn-dynamic-import": "^4.0.0", + "acorn-walk": "^6.1.0", "xtend": "^4.0.1" - }, - "dependencies": { - "acorn": { - "version": "6.0.2", - "resolved": "https://registry.npmjs.org/acorn/-/acorn-6.0.2.tgz", - "integrity": "sha512-GXmKIvbrN3TV7aVqAzVFaMW8F8wzVX7voEBRO3bDA64+EX37YSayggRJP5Xig6HYHBkWKpFg9W5gg6orklubhg==" - } } }, "acorn-walk": { - "version": "6.1.0", - "resolved": "https://registry.npmjs.org/acorn-walk/-/acorn-walk-6.1.0.tgz", - "integrity": "sha512-ugTb7Lq7u4GfWSqqpwE0bGyoBZNMTok/zDBXxfEG0QM50jNlGhIWjRC1pPN7bvV1anhF+bs+/gNcRw+o55Evbg==" + "version": "6.1.1", + "resolved": "https://registry.npmjs.org/acorn-walk/-/acorn-walk-6.1.1.tgz", + "integrity": "sha512-OtUw6JUTgxA2QoqqmrmQ7F2NYqiBPi/L2jqHyFtllhOUvXYQXf0Z1CYUinIfyT4bTCGmrA7gX9FvHA81uzCoVw==" }, "agent-base": { "version": "4.2.1", @@ -444,14 +476,14 @@ } }, "ajv": { - "version": "5.5.2", - "resolved": "https://registry.npmjs.org/ajv/-/ajv-5.5.2.tgz", - "integrity": "sha1-c7Xuyj+rZT49P5Qis0GtQiBdyWU=", + "version": "6.5.5", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.5.5.tgz", + "integrity": "sha512-7q7gtRQDJSyuEHjuVgHoUa2VuemFiCMrfQc9Tc08XTAc4Zj/5U1buQJ0HU6i7fKjXU09SVgSmxa4sLvuvS8Iyg==", "requires": { - "co": "^4.6.0", - "fast-deep-equal": "^1.0.0", + "fast-deep-equal": "^2.0.1", "fast-json-stable-stringify": "^2.0.0", - "json-schema-traverse": "^0.3.0" + "json-schema-traverse": "^0.4.1", + "uri-js": "^4.2.2" } }, "ajv-keywords": { @@ -523,6 +555,15 @@ "integrity": "sha1-ZlWX3oap/+Oqm/vmyuXG6kJrSXk=", "dev": true }, + "append-transform": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/append-transform/-/append-transform-1.0.0.tgz", + "integrity": "sha512-P009oYkeHyU742iSZJzZZywj4QRJdnTWffaKuJQLablCZ1uz6/cW4yaRgcDaoQ+uwOxxnt0gRUcwfsNP2ri0gw==", + "dev": true, + "requires": { + "default-require-extensions": "^2.0.0" + } + }, "archy": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/archy/-/archy-1.0.0.tgz", @@ -573,21 +614,6 @@ "resolved": "https://registry.npmjs.org/array-reduce/-/array-reduce-0.0.0.tgz", "integrity": "sha1-FziZ0//Rx9k4PkR5Ul2+J4yrXys=" }, - "array-union": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/array-union/-/array-union-1.0.2.tgz", - "integrity": "sha1-mjRBDk9OPaI96jdb5b5w8kd47Dk=", - "dev": true, - "requires": { - "array-uniq": "^1.0.1" - } - }, - "array-uniq": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/array-uniq/-/array-uniq-1.0.3.tgz", - "integrity": "sha1-r2rId6Jcx/dOBYiUdThY39sk/bY=", - "dev": true - }, "array-unique": { "version": "0.3.2", "resolved": "https://registry.npmjs.org/array-unique/-/array-unique-0.3.2.tgz", @@ -1447,15 +1473,14 @@ "integrity": "sha512-ccav/yGvoa80BQDljCxsmmQ3Xvx60/UpBIij5QN21W3wBi/hhIC9OoO+KLpu9IJTS9j4DRVJ3aDDF9cMSoa2lw==" }, "base64url": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/base64url/-/base64url-3.0.0.tgz", - "integrity": "sha512-LIVmqIrIWuiqTvn4RzcrwCOuHo2DD6tKmKBPXXlr4p4n4l6BZBkwFTIa3zu1XkX5MbZgro4a6BvPi+n2Mns5Gg==" + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/base64url/-/base64url-3.0.1.tgz", + "integrity": "sha512-ir1UPr3dkwexU7FdV8qBBbNDRUhMmIekYMFZfi+C/sLNnRESKPl23nB9b2pltqfOQNnGzsDdId90AEtG5tCx4A==" }, "bcrypt-pbkdf": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/bcrypt-pbkdf/-/bcrypt-pbkdf-1.0.2.tgz", "integrity": "sha1-pDAdOJtqQ/m2f/PKEaP2Y342Dp4=", - "optional": true, "requires": { "tweetnacl": "^0.14.3" } @@ -1494,9 +1519,19 @@ "requires": { "safer-buffer": ">= 2.1.2 < 3" } + }, + "qs": { + "version": "6.5.2", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.5.2.tgz", + "integrity": "sha512-N5ZAX4/LxJmF+7wN74pUD6qAh9/wnvdQcjq9TZjevvXzSUo7bfmw91saqMjzGS2xq91/odN2dW/WOl7qQHNDGA==" } } }, + "boolbase": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/boolbase/-/boolbase-1.0.0.tgz", + "integrity": "sha1-aN/1++YMUes3cl6p4+0xDcwed24=" + }, "bootstrap": { "version": "3.3.7", "resolved": "http://registry.npmjs.org/bootstrap/-/bootstrap-3.3.7.tgz", @@ -1909,9 +1944,9 @@ "integrity": "sha1-FkpUg+Yw+kMh5a8HAg5TGDGyYJs=" }, "caniuse-lite": { - "version": "1.0.30000907", - "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30000907.tgz", - "integrity": "sha512-No5sQ/OB2Nmka8MNOOM6nJx+Hxt6MQ6h7t7kgJFu9oTuwjykyKRSBP/+i/QAyFHxeHB+ddE0Da1CG5ihx9oehQ==" + "version": "1.0.30000948", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30000948.tgz", + "integrity": "sha512-Lw4y7oz1X5MOMZm+2IFaSISqVVQvUuD+ZUSfeYK/SlYiMjkHN/eJ2PDfJehW5NA6JjrxYSSnIWfwjeObQMEjFQ==" }, "capture-stack-trace": { "version": "1.0.1", @@ -1962,12 +1997,38 @@ "integrity": "sha1-tUc7M9yXxCTl2Y3IfVXU2KKci/I=", "dev": true }, + "chat-pane": { + "version": "1.2.7", + "resolved": "https://registry.npmjs.org/chat-pane/-/chat-pane-1.2.7.tgz", + "integrity": "sha512-itsMQWUNjajgdEiTNaRMl/7y9FEpCNrPNldfu+Dj+iJPE5Y0OIBOwzx6hoyItiJYsQSfMcsFiGss2Ie5U4YZtQ==", + "requires": { + "babel-preset-env": "^1.6.1", + "babel-preset-metalab": "^1.0.0", + "mime-types": "^2.1.13", + "pane-registry": ">1.0.0", + "rdflib": ">=0.17.0", + "solid-ui": ">=0.12.0" + } + }, "check-error": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/check-error/-/check-error-1.0.2.tgz", "integrity": "sha1-V00xLt2Iu13YkS6Sht1sCu1KrII=", "dev": true }, + "cheerio": { + "version": "1.0.0-rc.2", + "resolved": "https://registry.npmjs.org/cheerio/-/cheerio-1.0.0-rc.2.tgz", + "integrity": "sha1-S59TqBsn5NXawxwP/Qz6A8xoMNs=", + "requires": { + "css-select": "~1.2.0", + "dom-serializer": "~0.1.0", + "entities": "~1.1.1", + "htmlparser2": "^3.9.1", + "lodash": "^4.15.0", + "parse5": "^3.0.1" + } + }, "ci-info": { "version": "1.6.0", "resolved": "https://registry.npmjs.org/ci-info/-/ci-info-1.6.0.tgz", @@ -2066,7 +2127,8 @@ "co": { "version": "4.6.0", "resolved": "https://registry.npmjs.org/co/-/co-4.6.0.tgz", - "integrity": "sha1-bqa989hTrlTMuOR7+gvz+QMfsYQ=" + "integrity": "sha1-bqa989hTrlTMuOR7+gvz+QMfsYQ=", + "dev": true }, "code-point-at": { "version": "1.1.0", @@ -2118,10 +2180,10 @@ "source-map": "~0.5.3" }, "dependencies": { - "convert-source-map": { - "version": "1.1.3", - "resolved": "http://registry.npmjs.org/convert-source-map/-/convert-source-map-1.1.3.tgz", - "integrity": "sha1-SCnId+n+SbMWHzvzZziI4gRpmGA=" + "source-map": { + "version": "0.5.7", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.5.7.tgz", + "integrity": "sha1-igOdLRAh0i0eoUyA2OpGi6LvP8w=" } } }, @@ -2224,6 +2286,19 @@ "resolved": "https://registry.npmjs.org/constants-browserify/-/constants-browserify-1.0.0.tgz", "integrity": "sha1-wguW2MYXdIqvHBYCF2DNJ/y4y3U=" }, + "contacts-pane": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/contacts-pane/-/contacts-pane-1.0.3.tgz", + "integrity": "sha512-LgI8te1/2y9aOv909ak3BqgL5dbwDzlhSYcGnP/JBzsnZYuB19I89ybKkyJSiJVinVwZ6OJWfLgpwW7/EtXozg==", + "requires": { + "babel-preset-env": "^1.6.1", + "babel-preset-metalab": "^1.0.0", + "mime-types": "^2.1.13", + "pane-registry": ">=1.0.0", + "rdflib": ">=0.17.0", + "solid-ui": ">=0.11.3" + } + }, "content-disposition": { "version": "0.5.2", "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-0.5.2.tgz", @@ -2234,6 +2309,11 @@ "resolved": "https://registry.npmjs.org/content-type/-/content-type-1.0.4.tgz", "integrity": "sha512-hIP3EEPs8tB9AT1L+NUqtwOAps4mk2Zob89MWXMHjHWg9milF/j4osnnQLXBCBFBk/tvIG/tUc9mOUJiPBhPXA==" }, + "convert-source-map": { + "version": "1.1.3", + "resolved": "http://registry.npmjs.org/convert-source-map/-/convert-source-map-1.1.3.tgz", + "integrity": "sha1-SCnId+n+SbMWHzvzZziI4gRpmGA=" + }, "cookie": { "version": "0.3.1", "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.3.1.tgz", @@ -2256,9 +2336,9 @@ "integrity": "sha1-Z29us8OZl8LuGsOpJP1hJHSPV40=" }, "core-js": { - "version": "2.5.7", - "resolved": "https://registry.npmjs.org/core-js/-/core-js-2.5.7.tgz", - "integrity": "sha512-RszJCAxg/PP6uzXVXL6BsxSXx/B05oJAQ2vkJRjyjrEcNVycaqOmNb5OTxZPE3xa5gwZduqza6L9JOCenh/Ecw==" + "version": "2.6.5", + "resolved": "https://registry.npmjs.org/core-js/-/core-js-2.6.5.tgz", + "integrity": "sha512-klh/kDpwX8hryYL14M9w/xei6vrv6sE8gTHDG7/T/+SEovB/G4ejwcfE/CBzO6Edsu+OETZMZ3wcX/EjUkrl5A==" }, "core-util-is": { "version": "1.0.2", @@ -2369,6 +2449,33 @@ "integrity": "sha1-ojD2T1aDEOFJgAmUB5DsmVRbyn4=", "dev": true }, + "css-select": { + "version": "1.2.0", + "resolved": "http://registry.npmjs.org/css-select/-/css-select-1.2.0.tgz", + "integrity": "sha1-KzoRBTnFNV8c2NMUYj6HCxIeyFg=", + "requires": { + "boolbase": "~1.0.0", + "css-what": "2.1", + "domutils": "1.5.1", + "nth-check": "~1.0.1" + }, + "dependencies": { + "domutils": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/domutils/-/domutils-1.5.1.tgz", + "integrity": "sha1-3NhIiib1Y9YQeeSMn3t+Mjc2gs8=", + "requires": { + "dom-serializer": "0", + "domelementtype": "1" + } + } + } + }, + "css-what": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/css-what/-/css-what-2.1.2.tgz", + "integrity": "sha512-wan8dMWQ0GUeF7DGEPVjhHemVW/vy6xUYmFzRY8RYqgA0JtXC9rJmbScBjqSu6dg9q0lwPQy6ZAmJVr3PPTvqQ==" + }, "d": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/d/-/d-1.0.0.tgz", @@ -2457,6 +2564,15 @@ "integrity": "sha1-s2nW+128E+7PUk+RsHD+7cNXzzQ=", "dev": true }, + "default-require-extensions": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/default-require-extensions/-/default-require-extensions-2.0.0.tgz", + "integrity": "sha1-9fj7sYp9bVCyH2QfZJ67Uiz+JPc=", + "dev": true, + "requires": { + "strip-bom": "^3.0.0" + } + }, "define-properties": { "version": "1.1.3", "resolved": "https://registry.npmjs.org/define-properties/-/define-properties-1.1.3.tgz", @@ -2532,29 +2648,6 @@ "uniq": "^1.0.1" } }, - "del": { - "version": "2.2.2", - "resolved": "https://registry.npmjs.org/del/-/del-2.2.2.tgz", - "integrity": "sha1-wSyYHQZ4RshLyvhiz/kw2Qf/0ag=", - "dev": true, - "requires": { - "globby": "^5.0.0", - "is-path-cwd": "^1.0.0", - "is-path-in-cwd": "^1.0.0", - "object-assign": "^4.0.1", - "pify": "^2.0.0", - "pinkie-promise": "^2.0.0", - "rimraf": "^2.2.8" - }, - "dependencies": { - "pify": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/pify/-/pify-2.3.0.tgz", - "integrity": "sha1-7RQaasBDqEnqWISY59yosVMw6Qw=", - "dev": true - } - } - }, "delayed-stream": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", @@ -2665,11 +2758,49 @@ } } }, + "dom-serializer": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/dom-serializer/-/dom-serializer-0.1.0.tgz", + "integrity": "sha1-BzxpdUbOB4DOI75KKOKT5AvDDII=", + "requires": { + "domelementtype": "~1.1.1", + "entities": "~1.1.1" + }, + "dependencies": { + "domelementtype": { + "version": "1.1.3", + "resolved": "http://registry.npmjs.org/domelementtype/-/domelementtype-1.1.3.tgz", + "integrity": "sha1-vSh3PiZCiBrsUVRJJCmcXNgiGFs=" + } + } + }, "domain-browser": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/domain-browser/-/domain-browser-1.2.0.tgz", "integrity": "sha512-jnjyiM6eRyZl2H+W8Q/zLMA481hzi0eszAaBUzIVnmYVDBbnLxVNnfu1HgEBvCbL+71FrxMl3E6lpKH7Ge3OXA==" }, + "domelementtype": { + "version": "1.3.0", + "resolved": "http://registry.npmjs.org/domelementtype/-/domelementtype-1.3.0.tgz", + "integrity": "sha1-sXrtguirWeUt2cGbF1bg/BhyBMI=" + }, + "domhandler": { + "version": "2.4.2", + "resolved": "https://registry.npmjs.org/domhandler/-/domhandler-2.4.2.tgz", + "integrity": "sha512-JiK04h0Ht5u/80fdLMCEmV4zkNh2BcoMFBmZ/91WtYZ8qVXSKjiw7fXMgFPnHcSZgOo3XdinHvmnDUeMf5R4wA==", + "requires": { + "domelementtype": "1" + } + }, + "domutils": { + "version": "1.7.0", + "resolved": "https://registry.npmjs.org/domutils/-/domutils-1.7.0.tgz", + "integrity": "sha512-Lgd2XcJ/NjEw+7tFvfKxOzCYKZsdct5lczQ2ZaQY8Djz7pfAD3Gbp8ySJWtreII/vDlMVmxwa6pHmdxIYgttDg==", + "requires": { + "dom-serializer": "0", + "domelementtype": "1" + } + }, "dot-prop": { "version": "4.2.0", "resolved": "https://registry.npmjs.org/dot-prop/-/dot-prop-4.2.0.tgz", @@ -2726,7 +2857,6 @@ "version": "0.1.2", "resolved": "https://registry.npmjs.org/ecc-jsbn/-/ecc-jsbn-0.1.2.tgz", "integrity": "sha1-OoOpBOVDUyh4dMVkt1SThoSamMk=", - "optional": true, "requires": { "jsbn": "~0.1.0", "safer-buffer": "^2.1.0" @@ -2738,9 +2868,9 @@ "integrity": "sha1-WQxhFWsK4vTwJVcyoViyZrxWsh0=" }, "electron-to-chromium": { - "version": "1.3.84", - "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.3.84.tgz", - "integrity": "sha512-IYhbzJYOopiTaNWMBp7RjbecUBsbnbDneOP86f3qvS0G0xfzwNSvMJpTrvi5/Y1gU7tg2NAgeg8a8rCYvW9Whw==" + "version": "1.3.116", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.3.116.tgz", + "integrity": "sha512-NKwKAXzur5vFCZYBHpdWjTMO8QptNLNP80nItkSIgUOapPAo9Uia+RvkCaZJtO7fhQaVElSvBPWEc2ku6cKsPA==" }, "elliptic": { "version": "6.4.1", @@ -2775,6 +2905,11 @@ "iconv-lite": "~0.4.13" } }, + "entities": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/entities/-/entities-1.1.2.tgz", + "integrity": "sha512-f2LZMYl1Fzu7YSBKg+RoROelpOaNrcGmE9AZubeDfrCEia483oW4MI4VyFd5VNHIgQ/7qm1I0wUHK1eJnn2y2w==" + }, "es5-ext": { "version": "0.10.46", "resolved": "https://registry.npmjs.org/es5-ext/-/es5-ext-0.10.46.tgz", @@ -2889,15 +3024,6 @@ "esutils": "^2.0.2", "optionator": "^0.8.1", "source-map": "~0.6.1" - }, - "dependencies": { - "source-map": { - "version": "0.6.1", - "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", - "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", - "dev": true, - "optional": true - } } }, "escope": { @@ -3043,6 +3169,14 @@ "requires": { "acorn": "^5.5.0", "acorn-jsx": "^3.0.0" + }, + "dependencies": { + "acorn": { + "version": "5.7.3", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-5.7.3.tgz", + "integrity": "sha512-T/zvzYRfbVojPWahDsE5evJdHb3oJoQfFbsrKM7w5Zcs++Tr257tia3BmMP8XYVjp1S9RZXQMh7gao96BlqZOw==", + "dev": true + } } }, "esprima": { @@ -3194,6 +3328,11 @@ "vary": "~1.1.2" }, "dependencies": { + "qs": { + "version": "6.5.2", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.5.2.tgz", + "integrity": "sha512-N5ZAX4/LxJmF+7wN74pUD6qAh9/wnvdQcjq9TZjevvXzSUo7bfmw91saqMjzGS2xq91/odN2dW/WOl7qQHNDGA==" + }, "statuses": { "version": "1.4.0", "resolved": "https://registry.npmjs.org/statuses/-/statuses-1.4.0.tgz", @@ -3342,9 +3481,9 @@ "integrity": "sha1-lpGEQOMEGnpBT4xS48V06zw+HgU=" }, "fast-deep-equal": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-1.1.0.tgz", - "integrity": "sha1-wFNHeBfIa1HaqFPIHgWbcz0CNhQ=" + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-2.0.1.tgz", + "integrity": "sha1-ewUhjd+WZ79/Nwv3/bLLFf3Qqkk=" }, "fast-json-stable-stringify": { "version": "2.0.0", @@ -3425,9 +3564,9 @@ } }, "find-babel-config": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/find-babel-config/-/find-babel-config-1.1.0.tgz", - "integrity": "sha1-rMAQQ6Z0n+w0Qpvmtk9ULrtdY1U=", + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/find-babel-config/-/find-babel-config-1.2.0.tgz", + "integrity": "sha512-jB2CHJeqy6a820ssiqwrKMeyC6nNdmrcgkKWJWmpoxpE8RKciYJXCcXRq1h2AzCo5I5BJeN2tkGEO3hLTuePRA==", "requires": { "json5": "^0.5.1", "path-exists": "^3.0.0" @@ -3440,21 +3579,34 @@ "dev": true }, "flat-cache": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-1.3.0.tgz", - "integrity": "sha1-0wMLMrOBVPTjt+nHCfSQ9++XxIE=", + "version": "1.3.4", + "resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-1.3.4.tgz", + "integrity": "sha512-VwyB3Lkgacfik2vhqR4uv2rvebqmDvFu4jlN/C1RzWoJEo8I7z4Q404oiqYCkq41mni8EzQnm95emU9seckwtg==", "dev": true, "requires": { "circular-json": "^0.3.1", - "del": "^2.0.2", "graceful-fs": "^4.1.2", + "rimraf": "~2.6.2", "write": "^0.2.1" } }, + "folder-pane": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/folder-pane/-/folder-pane-1.0.4.tgz", + "integrity": "sha512-sWDOsJNxCujt7fRqiX0N58jPnM8WP0jCl8E8+ICECKvEgD4PEPQyrhGegopy5uL5yaqaGjmVKZLmTld/jCMBCQ==", + "requires": { + "babel-preset-env": "^1.6.1", + "babel-preset-metalab": "^1.0.0", + "mime-types": "^2.1.13", + "pane-registry": "*", + "rdflib": ">=0.17.0", + "solid-ui": ">=0.11.3" + } + }, "follow-redirects": { - "version": "1.5.8", - "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.5.8.tgz", - "integrity": "sha512-sy1mXPmv7kLAMKW/8XofG7o9T+6gAjzdZK4AJF6ryqQYUa/hnzgiypoeUecZ53x7XiqKNEpNqLtS97MshW2nxg==", + "version": "1.5.10", + "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.5.10.tgz", + "integrity": "sha512-0V5l4Cizzvqt5D44aTXbFZz+FtyXV1vrDN6qrelxtfYQKW0KO0W2T/hkE8xvGa/540LkZlkaUjO4ailYTFtHVQ==", "requires": { "debug": "=3.1.0" }, @@ -3489,23 +3641,13 @@ "integrity": "sha1-+8cfDEGt6zf5bFd60e1C2P2sypE=" }, "form-data": { - "version": "2.3.2", - "resolved": "https://registry.npmjs.org/form-data/-/form-data-2.3.2.tgz", - "integrity": "sha1-SXBJi+YEwgwAXU9cI67NIda0kJk=", + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/form-data/-/form-data-2.3.3.tgz", + "integrity": "sha512-1lLKB2Mu3aGP1Q/2eCOx0fNbRMe7XdwktwOruhfqqd0rIJWwN4Dh+E3hrPSlDCXnSR7UtZ1N38rVXm+6+MEhJQ==", "requires": { "asynckit": "^0.4.0", - "combined-stream": "1.0.6", + "combined-stream": "^1.0.6", "mime-types": "^2.1.12" - }, - "dependencies": { - "combined-stream": { - "version": "1.0.6", - "resolved": "http://registry.npmjs.org/combined-stream/-/combined-stream-1.0.6.tgz", - "integrity": "sha1-cj599ugBrFYTETp+RFqbactjKBg=", - "requires": { - "delayed-stream": "~1.0.0" - } - } } }, "formatio": { @@ -3745,14 +3887,14 @@ } }, "global-tunnel-ng": { - "version": "2.6.0", - "resolved": "https://registry.npmjs.org/global-tunnel-ng/-/global-tunnel-ng-2.6.0.tgz", - "integrity": "sha512-glWGTgPzsOQs0mPRxHnWIwqYrEuQcxYpUFWF7BJxJL+c2F4fW304vdn53pqgod4PzOqZKDr1cex+a/pXCwrncA==", + "version": "2.7.1", + "resolved": "https://registry.npmjs.org/global-tunnel-ng/-/global-tunnel-ng-2.7.1.tgz", + "integrity": "sha512-4s+DyciWBV0eK148wqXxcmVAbFVPqtc3sEtUE/GTQfuU80rySLcMhUmHKSHI7/LDj8q0gDYI1lIhRRB7ieRAqg==", "requires": { "encodeurl": "^1.0.2", "lodash": "^4.17.10", "npm-conf": "^1.1.3", - "tunnel": "^0.0.5" + "tunnel": "^0.0.6" } }, "globals": { @@ -3760,28 +3902,6 @@ "resolved": "https://registry.npmjs.org/globals/-/globals-9.18.0.tgz", "integrity": "sha512-S0nG3CLEQiY/ILxqtztTWH/3iRRdyBLw6KMDxnKMchrtbj2OFmehVh0WUCfW3DUrIgx/qFrJPICrq4Z4sTR9UQ==" }, - "globby": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/globby/-/globby-5.0.0.tgz", - "integrity": "sha1-69hGZ8oNuzMLmbz8aOrCvFQ3Dg0=", - "dev": true, - "requires": { - "array-union": "^1.0.1", - "arrify": "^1.0.0", - "glob": "^7.0.3", - "object-assign": "^4.0.1", - "pify": "^2.0.0", - "pinkie-promise": "^2.0.0" - }, - "dependencies": { - "pify": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/pify/-/pify-2.3.0.tgz", - "integrity": "sha1-7RQaasBDqEnqWISY59yosVMw6Qw=", - "dev": true - } - } - }, "got": { "version": "6.7.1", "resolved": "https://registry.npmjs.org/got/-/got-6.7.1.tgz", @@ -3802,9 +3922,9 @@ } }, "graceful-fs": { - "version": "4.1.11", - "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.1.11.tgz", - "integrity": "sha1-Dovf5NHduIVNZOBOp8AOKgJuVlg=" + "version": "4.1.15", + "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.1.15.tgz", + "integrity": "sha512-6uHUhOPEBgQ24HM+r6b/QwWfZq+yiFcipKFrOFiBEnWdy5sdzYoi+pJeQaPI5qOLRFqWmAXUPQNsielzdLoecA==" }, "graphlib": { "version": "2.1.7", @@ -3839,11 +3959,6 @@ "requires": { "lodash": "^4.17.11" } - }, - "source-map": { - "version": "0.6.1", - "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", - "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==" } } }, @@ -3853,11 +3968,11 @@ "integrity": "sha1-qUwiJOvKwEeCoNkDVSHyRzW37JI=" }, "har-validator": { - "version": "5.1.0", - "resolved": "https://registry.npmjs.org/har-validator/-/har-validator-5.1.0.tgz", - "integrity": "sha512-+qnmNjI4OfH2ipQ9VQOw23bBd/ibtfbVdK2fYbY4acTDqKTW/YDp9McimZdDbG8iV9fZizUqQMD5xvriB146TA==", + "version": "5.1.3", + "resolved": "https://registry.npmjs.org/har-validator/-/har-validator-5.1.3.tgz", + "integrity": "sha512-sNvOCzEQNr/qrvJgc3UG/kD4QtlHycrzwS+6mfTrrSq97BvaYcPZZI1ZSqGSPR73Cxn4LKTD4PttRwfU7jWq5g==", "requires": { - "ajv": "^5.3.0", + "ajv": "^6.5.5", "har-schema": "^2.0.0" } }, @@ -3972,6 +4087,39 @@ "resolved": "https://registry.npmjs.org/htmlescape/-/htmlescape-1.1.1.tgz", "integrity": "sha1-OgPtwiFLyjtmQko+eVk0lQnLA1E=" }, + "htmlparser2": { + "version": "3.10.0", + "resolved": "https://registry.npmjs.org/htmlparser2/-/htmlparser2-3.10.0.tgz", + "integrity": "sha512-J1nEUGv+MkXS0weHNWVKJJ+UrLfePxRWpN3C9bEi9fLxL2+ggW94DQvgYVXsaT30PGwYRIZKNZXuyMhp3Di4bQ==", + "requires": { + "domelementtype": "^1.3.0", + "domhandler": "^2.3.0", + "domutils": "^1.5.1", + "entities": "^1.1.1", + "inherits": "^2.0.1", + "readable-stream": "^3.0.6" + }, + "dependencies": { + "readable-stream": { + "version": "3.0.6", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.0.6.tgz", + "integrity": "sha512-9E1oLoOWfhSXHGv6QlwXJim7uNzd9EVlWK+21tCU9Ju/kR0/p2AZYPz4qSchgO8PlLIH4FpZYfzwS+rEksZjIg==", + "requires": { + "inherits": "^2.0.3", + "string_decoder": "^1.1.1", + "util-deprecate": "^1.0.1" + } + }, + "string_decoder": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", + "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", + "requires": { + "safe-buffer": "~5.1.0" + } + } + } + }, "http-errors": { "version": "1.6.3", "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-1.6.3.tgz", @@ -4129,6 +4277,13 @@ "integrity": "sha1-+Tk0ccGKedFyT4Y/o4tYY3Ct4qU=", "requires": { "source-map": "~0.5.3" + }, + "dependencies": { + "source-map": { + "version": "0.5.7", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.5.7.tgz", + "integrity": "sha1-igOdLRAh0i0eoUyA2OpGi6LvP8w=" + } } }, "inquirer": { @@ -4378,21 +4533,6 @@ "integrity": "sha1-PkcprB9f3gJc19g6iW2rn09n2w8=", "dev": true }, - "is-path-cwd": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/is-path-cwd/-/is-path-cwd-1.0.0.tgz", - "integrity": "sha1-0iXsIxMuie3Tj9p2dHLmLmXxEG0=", - "dev": true - }, - "is-path-in-cwd": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/is-path-in-cwd/-/is-path-in-cwd-1.0.1.tgz", - "integrity": "sha512-FjV1RTW48E7CWM7eE/J2NJvAEEVektecDBVBE5Hh3nM1Jd0kvhHtX68Pr3xsDf857xt3Y4AkwVULK1Vku62aaQ==", - "dev": true, - "requires": { - "is-path-inside": "^1.0.0" - } - }, "is-path-inside": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/is-path-inside/-/is-path-inside-1.0.1.tgz", @@ -4501,6 +4641,19 @@ "resolved": "https://registry.npmjs.org/isstream/-/isstream-0.1.2.tgz", "integrity": "sha1-R+Y/evVa+m+S4VAOaQ64uFKcCZo=" }, + "issue-pane": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/issue-pane/-/issue-pane-1.0.4.tgz", + "integrity": "sha512-wxDXpwmq4DQ2Fb6aRsNpFhaWTtcfnx6n+puOM5fHih1Im8gHy8C+qvufNgqfq1SlipFzskXOc8Cre61tj4xEuA==", + "requires": { + "babel-preset-env": "^1.6.1", + "babel-preset-metalab": "^1.0.0", + "mime-types": "^2.1.13", + "pane-registry": "^1.0.4", + "rdflib": ">=0.17.0", + "solid-ui": ">=0.11.3" + } + }, "istanbul-lib-coverage": { "version": "2.0.3", "resolved": "https://registry.npmjs.org/istanbul-lib-coverage/-/istanbul-lib-coverage-2.0.3.tgz", @@ -4548,8 +4701,7 @@ "jsbn": { "version": "0.1.1", "resolved": "https://registry.npmjs.org/jsbn/-/jsbn-0.1.1.tgz", - "integrity": "sha1-peZUwuWi3rXyAdls77yoDA7y9RM=", - "optional": true + "integrity": "sha1-peZUwuWi3rXyAdls77yoDA7y9RM=" }, "jsesc": { "version": "0.5.0", @@ -4562,9 +4714,9 @@ "integrity": "sha1-tIDIkuWaLwWVTOcnvT8qTogvnhM=" }, "json-schema-traverse": { - "version": "0.3.1", - "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.3.1.tgz", - "integrity": "sha1-NJptRMU6Ud6JtAgFxdXlm0F9M0A=" + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", + "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==" }, "json-stable-stringify": { "version": "0.0.1", @@ -4766,9 +4918,9 @@ } }, "localstorage-memory": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/localstorage-memory/-/localstorage-memory-1.0.2.tgz", - "integrity": "sha1-zUqPIQ5V3VGckp9LTMgoKbWPmlE=", + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/localstorage-memory/-/localstorage-memory-1.0.3.tgz", + "integrity": "sha512-t9P8WB6DcVttbw/W4PIE8HOqum8Qlvx5SjR6oInwR9Uia0EEmyUeBh7S+weKByW+l/f45Bj4L/dgZikGFDM6ng==", "dev": true }, "lodash": { @@ -4856,6 +5008,14 @@ "requires": { "pseudomap": "^1.0.2", "yallist": "^2.1.2" + }, + "dependencies": { + "yallist": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-2.1.2.tgz", + "integrity": "sha1-HBH5IY8HYImkfdUS+TxmmaaoHVI=", + "dev": true + } } }, "macos-release": { @@ -4887,12 +5047,12 @@ } }, "mashlib": { - "version": "0.7.17", - "resolved": "https://registry.npmjs.org/mashlib/-/mashlib-0.7.17.tgz", - "integrity": "sha512-asVogWGHOSaFkEb1YZZL+qOGHv26m8IaYBy83E5HYbXw0DaUA11FXxK3+hC+d1ite3y+d66JLvOZ5CyrXELQwg==", + "version": "0.7.18", + "resolved": "https://registry.npmjs.org/mashlib/-/mashlib-0.7.18.tgz", + "integrity": "sha512-PeZSqIX31WRq4WBmhUylguSHK4pyHmRduhvasumbO2FW5gvsG5QvypRQKkc4BBQtSBgh9ozE8C0ktOYU0fVpog==", "requires": { "rdflib": ">=0.19.1", - "solid-panes": ">=1.1.19" + "solid-panes": ">=1.3.1" }, "dependencies": { "rdflib": { @@ -4906,18 +5066,6 @@ "solid-auth-client": "^2.2.3", "xmldom": "^0.1.22" } - }, - "solid-auth-client": { - "version": "2.2.11", - "resolved": "https://registry.npmjs.org/solid-auth-client/-/solid-auth-client-2.2.11.tgz", - "integrity": "sha512-5x8K32OueXo5NPBrUasAbInObRJuSOmYOaTQzmqjRl+sF3U5OLrJASDNaWM3OzUkwqxPLI9mkp+xaBwaMS7yZg==", - "requires": { - "@babel/runtime": "^7.0.0", - "@solid/oidc-rp": "^0.8.0", - "auth-header": "^1.0.0", - "commander": "^2.11.0", - "isomorphic-fetch": "^2.2.1" - } } } }, @@ -4936,6 +5084,19 @@ "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-0.3.0.tgz", "integrity": "sha1-hxDXrwqmJvj/+hzgAWhUUmMlV0g=" }, + "meeting-pane": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/meeting-pane/-/meeting-pane-1.0.4.tgz", + "integrity": "sha512-mOaWBr1p0tUfzlyDYSeZOV7M6B3kegohhqAy/auTcgi/8papnSxtME5lRyqrvmlJoXO5vsPFPDnvbeevWWfmsg==", + "requires": { + "babel-preset-env": "^1.6.1", + "babel-preset-metalab": "^1.0.0", + "mime-types": "^2.1.13", + "pane-registry": ">1.0.0", + "rdflib": ">=0.17.0", + "solid-ui": ">=0.11.3" + } + }, "merge-descriptors": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-1.0.1.tgz", @@ -5018,9 +5179,9 @@ } }, "minimist": { - "version": "0.0.8", - "resolved": "http://registry.npmjs.org/minimist/-/minimist-0.0.8.tgz", - "integrity": "sha1-hX/Kv8M5fSYluCKCYuhqp6ARsF0=" + "version": "0.0.10", + "resolved": "http://registry.npmjs.org/minimist/-/minimist-0.0.10.tgz", + "integrity": "sha1-3j+YVD2/lggr5IrRoMfNqDYwHc8=" }, "mixin-deep": { "version": "1.3.1", @@ -5065,6 +5226,13 @@ "integrity": "sha1-MAV0OOrGz3+MR2fzhkjWaX11yQM=", "requires": { "minimist": "0.0.8" + }, + "dependencies": { + "minimist": { + "version": "0.0.8", + "resolved": "http://registry.npmjs.org/minimist/-/minimist-0.0.8.tgz", + "integrity": "sha1-hX/Kv8M5fSYluCKCYuhqp6ARsF0=" + } } }, "mocha": { @@ -5127,9 +5295,9 @@ } }, "module-deps": { - "version": "6.1.0", - "resolved": "https://registry.npmjs.org/module-deps/-/module-deps-6.1.0.tgz", - "integrity": "sha512-NPs5N511VD1rrVJihSso/LiBShRbJALYBKzDW91uZYy7BpjnO4bGnZL3HjZ9yKcFdZUWwaYjDz9zxbuP7vKMuQ==", + "version": "6.2.0", + "resolved": "https://registry.npmjs.org/module-deps/-/module-deps-6.2.0.tgz", + "integrity": "sha512-hKPmO06so6bL/ZvqVNVqdTVO8UAYsi3tQWlCa+z9KuWhoN4KDQtb5hcqQQv58qYiDE21wIvnttZEPiDgEbpwbA==", "requires": { "JSONStream": "^1.0.3", "browser-resolve": "^1.7.0", @@ -5300,23 +5468,23 @@ }, "dependencies": { "chai": { - "version": "4.1.2", - "resolved": "https://registry.npmjs.org/chai/-/chai-4.1.2.tgz", - "integrity": "sha1-D2RYS6ZC8PKs4oBiefTwbKI61zw=", + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/chai/-/chai-4.2.0.tgz", + "integrity": "sha512-XQU3bhBukrOsQCuwZndwGcCVQHyZi53fQ6Ys1Fym7E4olpIqqZZhhoFJoaKVvV17lWQoXYwgWN2nF5crA8J2jw==", "dev": true, "requires": { - "assertion-error": "^1.0.1", - "check-error": "^1.0.1", - "deep-eql": "^3.0.0", + "assertion-error": "^1.1.0", + "check-error": "^1.0.2", + "deep-eql": "^3.0.1", "get-func-name": "^2.0.0", - "pathval": "^1.0.0", - "type-detect": "^4.0.0" + "pathval": "^1.1.0", + "type-detect": "^4.0.5" } }, "debug": { - "version": "3.2.5", - "resolved": "https://registry.npmjs.org/debug/-/debug-3.2.5.tgz", - "integrity": "sha512-D61LaDQPQkxJ5AUM2mbSJRbPkNs/TmdmOeLAi1hgDkpDfIfetSrjmWhccwtuResSwMbACjx/xXQofvM9CE/aeg==", + "version": "3.2.6", + "resolved": "https://registry.npmjs.org/debug/-/debug-3.2.6.tgz", + "integrity": "sha512-mel+jf7nrtEl5Pn1Qx46zARXKDpBbvzezse7p7LqINmdoIk8PYP5SySaxEmYv6TZ0JyEKA1hsCId6DIhgITtWQ==", "dev": true, "requires": { "ms": "^2.1.1" @@ -5404,6 +5572,14 @@ "path-key": "^2.0.0" } }, + "nth-check": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/nth-check/-/nth-check-1.0.2.tgz", + "integrity": "sha512-WeBOdju8SnzPN5vTUJYxYUxLeXpCaVP5i5e0LF8fg7WORF2Wd7wFX/pk0tYZk7s8T+J7VLy0Da6J1+wCT0AtHg==", + "requires": { + "boolbase": "~1.0.0" + } + }, "number-is-nan": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/number-is-nan/-/number-is-nan-1.0.1.tgz", @@ -5443,25 +5619,8 @@ "dependencies": { "ansi-regex": { "version": "3.0.0", - "bundled": true, - "dev": true - }, - "append-transform": { - "version": "1.0.0", - "bundled": true, - "dev": true, - "requires": { - "default-require-extensions": "^2.0.0" - } - }, - "archy": { - "version": "1.0.0", - "bundled": true, - "dev": true - }, - "arrify": { - "version": "1.0.1", - "bundled": true, + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-3.0.0.tgz", + "integrity": "sha1-7QMXwyIGT3lGbAKWa922Bas32Zg=", "dev": true }, "async": { @@ -5514,8 +5673,7 @@ }, "code-point-at": { "version": "1.1.0", - "bundled": true, - "dev": true + "bundled": true }, "commander": { "version": "2.17.1", @@ -5535,7 +5693,8 @@ }, "convert-source-map": { "version": "1.6.0", - "bundled": true, + "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-1.6.0.tgz", + "integrity": "sha512-eFu7XigvxdZ1ETfbgPBohgyQ/Z++C0eEhTor0qRwBw9unw+L0/6V8wkSuGgzdThkiS5lSpdptOQPD8Ak40a+7A==", "dev": true, "requires": { "safe-buffer": "~5.1.1" @@ -5543,7 +5702,8 @@ }, "cross-spawn": { "version": "4.0.2", - "bundled": true, + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-4.0.2.tgz", + "integrity": "sha1-e5JHYhwjrf3ThWAEqCPL45dCTUE=", "dev": true, "requires": { "lru-cache": "^4.0.1", @@ -5560,16 +5720,7 @@ }, "decamelize": { "version": "1.2.0", - "bundled": true, - "dev": true - }, - "default-require-extensions": { - "version": "2.0.0", - "bundled": true, - "dev": true, - "requires": { - "strip-bom": "^3.0.0" - } + "bundled": true }, "end-of-stream": { "version": "1.4.1", @@ -5698,6 +5849,17 @@ "version": "0.6.1", "bundled": true, "dev": true + }, + "uglify-js": { + "version": "3.4.9", + "resolved": "https://registry.npmjs.org/uglify-js/-/uglify-js-3.4.9.tgz", + "integrity": "sha512-8CJsbKOtEbnJsTyv6LE6m6ZKniqMiFWmm9sRbopbkGs3gMPPfd3Fh8iIA4Ykv5MgaTbqHr4BaoGLJLZNhsrW1Q==", + "dev": true, + "optional": true, + "requires": { + "commander": "~2.17.1", + "source-map": "~0.6.1" + } } } }, @@ -5828,6 +5990,14 @@ "bundled": true, "dev": true }, + "kind-of": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz", + "integrity": "sha1-MeohpzS6ubuw8yRm2JOupR5KPGQ=", + "requires": { + "is-buffer": "^1.1.5" + } + }, "lcid": { "version": "2.0.0", "bundled": true, @@ -5980,8 +6150,7 @@ }, "number-is-nan": { "version": "1.0.1", - "bundled": true, - "dev": true + "bundled": true }, "once": { "version": "1.4.0", @@ -6260,7 +6429,8 @@ }, "string-width": { "version": "2.1.1", - "bundled": true, + "resolved": "https://registry.npmjs.org/string-width/-/string-width-2.1.1.tgz", + "integrity": "sha512-nOqH59deCq9SRHlxq1Aw85Jnt4w6KvLKqWVik6oA9ZklXLNIOlqg4F2yrT1MVaTjAqvVwdfeZ7w7aCvJD7ugkw==", "dev": true, "requires": { "is-fullwidth-code-point": "^2.0.0", @@ -6269,7 +6439,8 @@ }, "strip-ansi": { "version": "4.0.0", - "bundled": true, + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-4.0.0.tgz", + "integrity": "sha1-qEeQIusaw2iocTibY1JixQXuNo8=", "dev": true, "requires": { "ansi-regex": "^3.0.0" @@ -6297,20 +6468,20 @@ } }, "uglify-js": { - "version": "3.4.9", - "bundled": true, - "dev": true, - "optional": true, + "version": "2.8.29", + "resolved": "https://registry.npmjs.org/uglify-js/-/uglify-js-2.8.29.tgz", + "integrity": "sha1-KcVzMUgFe7Th913zW3qcty5qWd0=", "requires": { - "commander": "~2.17.1", - "source-map": "~0.6.1" + "yargs": "~3.10.0" }, "dependencies": { - "source-map": { - "version": "0.6.1", - "bundled": true, - "dev": true, - "optional": true + "yargs": { + "version": "3.10.0", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-3.10.0.tgz", + "integrity": "sha1-9+572FfdfB0tOMDnTvvWgdFDH9E=", + "requires": { + "decamelize": "^1.0.0" + } } } }, @@ -6346,48 +6517,6 @@ "bundled": true, "dev": true }, - "wrap-ansi": { - "version": "2.1.0", - "bundled": true, - "dev": true, - "requires": { - "string-width": "^1.0.1", - "strip-ansi": "^3.0.1" - }, - "dependencies": { - "ansi-regex": { - "version": "2.1.1", - "bundled": true, - "dev": true - }, - "is-fullwidth-code-point": { - "version": "1.0.0", - "bundled": true, - "dev": true, - "requires": { - "number-is-nan": "^1.0.0" - } - }, - "string-width": { - "version": "1.0.2", - "bundled": true, - "dev": true, - "requires": { - "code-point-at": "^1.0.0", - "is-fullwidth-code-point": "^1.0.0", - "strip-ansi": "^3.0.0" - } - }, - "strip-ansi": { - "version": "3.0.1", - "bundled": true, - "dev": true, - "requires": { - "ansi-regex": "^2.0.0" - } - } - } - }, "wrappy": { "version": "1.0.2", "bundled": true, @@ -6410,7 +6539,8 @@ }, "yallist": { "version": "2.1.2", - "bundled": true, + "resolved": "https://registry.npmjs.org/yallist/-/yallist-2.1.2.tgz", + "integrity": "sha1-HBH5IY8HYImkfdUS+TxmmaaoHVI=", "dev": true }, "yargs": { @@ -6707,6 +6837,18 @@ "resolved": "https://registry.npmjs.org/pako/-/pako-1.0.6.tgz", "integrity": "sha512-lQe48YPsMJAig+yngZ87Lus+NF+3mtu7DVOBu6b/gHO1YpKwIj5AWjZ/TOS7i46HD/UixzWb1zeWDZfGZ3iYcg==" }, + "pane-registry": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/pane-registry/-/pane-registry-1.0.4.tgz", + "integrity": "sha512-uX00yWpcjcNpaTjOHBvJyzFuWQIdF+S0oZTQOTXO8r7fpBhOMiw+fzEk3zeAE7SmLxJzbf2E/HTMusOx3eA6mg==", + "requires": { + "babel-preset-env": "^1.6.1", + "babel-preset-metalab": "^1.0.0", + "mime-types": "^2.1.13", + "rdflib": ">=0.17.0", + "solid-ui": ">=0.11.3" + } + }, "parents": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/parents/-/parents-1.0.1.tgz", @@ -6727,6 +6869,14 @@ "pbkdf2": "^3.0.3" } }, + "parse5": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/parse5/-/parse5-3.0.3.tgz", + "integrity": "sha512-rgO9Zg5LLLkfJF9E6CCmXlSE4UVceloys8JrFqCcHloC3usd/kJCyPDwH2SOlzix2j3xaP9sUX3e8+kvkuleAA==", + "requires": { + "@types/node": "*" + } + }, "parseurl": { "version": "1.3.2", "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.2.tgz", @@ -6920,6 +7070,16 @@ "integrity": "sha1-AMLa7t2iDofjeCs0Stuhzd1q1wk=", "dev": true }, + "proper-lockfile": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/proper-lockfile/-/proper-lockfile-3.2.0.tgz", + "integrity": "sha512-iMghHHXv2bsxl6NchhEaFck8tvX3F9cknEEh1SUpguUOBjN7PAAW9BLzmbc1g/mCD1gY3EE2EABBHPJfFdHFmA==", + "requires": { + "graceful-fs": "^4.1.11", + "retry": "^0.12.0", + "signal-exit": "^3.0.2" + } + }, "proto-list": { "version": "1.2.4", "resolved": "https://registry.npmjs.org/proto-list/-/proto-list-1.2.4.tgz", @@ -7003,9 +7163,9 @@ "integrity": "sha512-XRsRjdf+j5ml+y/6GKHPZbrF/8p2Yga0JPtdqTIY2Xe5ohJPD9saDJJLPvp9+NSBprVvevdXZybnj2cv8OEd0A==" }, "qs": { - "version": "6.5.2", - "resolved": "https://registry.npmjs.org/qs/-/qs-6.5.2.tgz", - "integrity": "sha512-N5ZAX4/LxJmF+7wN74pUD6qAh9/wnvdQcjq9TZjevvXzSUo7bfmw91saqMjzGS2xq91/odN2dW/WOl7qQHNDGA==" + "version": "6.6.0", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.6.0.tgz", + "integrity": "sha512-KIJqT9jQJDQx5h5uAVPimw6yVg2SekOKu959OCtktD3FjzbpvaPr8i4zzg07DOMz+igA4W/aNM7OV8H37pFYfA==" }, "querystring": { "version": "0.2.0", @@ -7310,6 +7470,13 @@ "tough-cookie": "~2.4.3", "tunnel-agent": "^0.6.0", "uuid": "^3.3.2" + }, + "dependencies": { + "qs": { + "version": "6.5.2", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.5.2.tgz", + "integrity": "sha512-N5ZAX4/LxJmF+7wN74pUD6qAh9/wnvdQcjq9TZjevvXzSUo7bfmw91saqMjzGS2xq91/odN2dW/WOl7qQHNDGA==" + } } }, "require-uncached": { @@ -7360,6 +7527,11 @@ "resolved": "https://registry.npmjs.org/ret/-/ret-0.1.15.tgz", "integrity": "sha512-TTlYpa+OL+vMMNG24xSlQGEJ3B/RzEfUlLct7b5G/ytav+wPrplCpVMFuwzXbkecJrb6IYo1iFb0S9v37754mg==" }, + "retry": { + "version": "0.12.0", + "resolved": "https://registry.npmjs.org/retry/-/retry-0.12.0.tgz", + "integrity": "sha1-G0KmJmoh8HQh0bC1S33BZ7AcATs=" + }, "rimraf": { "version": "2.6.2", "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-2.6.2.tgz", @@ -7447,9 +7619,9 @@ "dev": true }, "semver": { - "version": "5.5.1", - "resolved": "https://registry.npmjs.org/semver/-/semver-5.5.1.tgz", - "integrity": "sha512-PqpAxfrEhlSUWge8dwIp4tZnQ25DIOthpiaHNIthsjEFQD6EvqUKUDM7L8O2rShkFccYo1VjJR0coWfNkCubRw==" + "version": "5.6.0", + "resolved": "https://registry.npmjs.org/semver/-/semver-5.6.0.tgz", + "integrity": "sha512-RS9R6R35NYgQn++fkDWaOmqGoj4Ek9gGs+DPxNUZKuwE183xjJroKvyo1IzVFeXvUrvmALy6FWD5xrdJT25gMg==" }, "semver-diff": { "version": "2.1.0", @@ -7611,8 +7783,7 @@ "signal-exit": { "version": "3.0.2", "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.2.tgz", - "integrity": "sha1-tf3AjxKH6hF4Yo5BXiUTK3NkbG0=", - "dev": true + "integrity": "sha1-tf3AjxKH6hF4Yo5BXiUTK3NkbG0=" }, "simple-concat": { "version": "1.0.0", @@ -7700,6 +7871,11 @@ "requires": { "is-extendable": "^0.1.0" } + }, + "source-map": { + "version": "0.5.7", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.5.7.tgz", + "integrity": "sha1-igOdLRAh0i0eoUyA2OpGi6LvP8w=" } } }, @@ -8232,12 +8408,6 @@ "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.1.tgz", "integrity": "sha512-tgp+dl5cGk28utYktBsrFqA7HKgrhgPsg6Z/EfhWI4gl1Hwq8B/GmY/0oXZ6nF8hDVesS/FpnYaD/kOWhYQvyg==", "dev": true - }, - "semver": { - "version": "5.6.0", - "resolved": "https://registry.npmjs.org/semver/-/semver-5.6.0.tgz", - "integrity": "sha512-RS9R6R35NYgQn++fkDWaOmqGoj4Ek9gGs+DPxNUZKuwE183xjJroKvyo1IzVFeXvUrvmALy6FWD5xrdJT25gMg==", - "dev": true } } }, @@ -8393,9 +8563,9 @@ } }, "solid-auth-client": { - "version": "2.2.12", - "resolved": "https://registry.npmjs.org/solid-auth-client/-/solid-auth-client-2.2.12.tgz", - "integrity": "sha512-/oHKeNcFpb8WQ+Gf92rsxVhSXGua1tLFXlG9Chu8KeB6KiF5r6Bj2XuCNhOi20IbQmj4z0vDQ4IDPmMphjAWjA==", + "version": "2.2.13", + "resolved": "https://registry.npmjs.org/solid-auth-client/-/solid-auth-client-2.2.13.tgz", + "integrity": "sha512-r9edtl0SNEgsLVwjKp3HDcx90H4sb2yAjOpMta5E7OxXfaQTnhMIMjXVwNZDvadWQqIBjGryGf0oYItFTTKO7Q==", "requires": { "@babel/runtime": "^7.0.0", "@solid/oidc-rp": "^0.8.0", @@ -8418,53 +8588,37 @@ } }, "solid-panes": { - "version": "1.1.19", - "resolved": "https://registry.npmjs.org/solid-panes/-/solid-panes-1.1.19.tgz", - "integrity": "sha512-N++oJpmTh+Pneuw3jZUhrJnM3U1Cd75fW/T7Xi5b7Q6EdwSQ24PC/ME1Hc0+csBGtWk1yK/xTdJMZJci+mTv4A==", + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/solid-panes/-/solid-panes-1.3.1.tgz", + "integrity": "sha512-SGw98zxvsp9gNzi1pv7jPWC4+O3e4xySPePFipBA7WN6BlCPweOlg+7Bm6xp7kj0jFSydu3+ckdM78W/xxuKKQ==", "requires": { "@solid/better-simple-slideshow": "^0.1.0", "babel-preset-env": "^1.6.1", "babel-preset-metalab": "^1.0.0", + "chat-pane": ">=1.2.7", + "contacts-pane": "^1.0.3", + "folder-pane": "^1.0.4", + "issue-pane": "^1.0.4", + "meeting-pane": "^1.0.4", "mime-types": "^2.1.13", + "pane-registry": "^1.0.4", "rdflib": ">=0.17.1", - "solid-ui": ">=0.11.5" - } - }, - "solid-permissions": { - "version": "0.6.0", - "resolved": "https://registry.npmjs.org/solid-permissions/-/solid-permissions-0.6.0.tgz", - "integrity": "sha512-TiObEonB4l8VNa/6IOnNL0/7u3GKil4Bh/BiBB51AXOCuLahvTQ+LQc+C5KLHAykzyb9z5oM/BVOE3w8mzIm/A==", - "requires": { - "debug": "^3.1.0", - "solid-namespace": "0.1.0" - }, - "dependencies": { - "debug": { - "version": "3.2.5", - "resolved": "https://registry.npmjs.org/debug/-/debug-3.2.5.tgz", - "integrity": "sha512-D61LaDQPQkxJ5AUM2mbSJRbPkNs/TmdmOeLAi1hgDkpDfIfetSrjmWhccwtuResSwMbACjx/xXQofvM9CE/aeg==", - "requires": { - "ms": "^2.1.1" - } - }, - "ms": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.1.tgz", - "integrity": "sha512-tgp+dl5cGk28utYktBsrFqA7HKgrhgPsg6Z/EfhWI4gl1Hwq8B/GmY/0oXZ6nF8hDVesS/FpnYaD/kOWhYQvyg==" - } + "solid-ui": ">=0.12.0", + "source-pane": "^1.0.3" } }, "solid-ui": { - "version": "0.11.12", - "resolved": "https://registry.npmjs.org/solid-ui/-/solid-ui-0.11.12.tgz", - "integrity": "sha512-Cw2EEbooJnOlCuny3VmPVaPZ6byaQq3EA67B/j2mUhNJvQOtT1bHtqey6veeF9e8LMzBH3ndVeBCdf2awPfD/A==", + "version": "0.12.0", + "resolved": "https://registry.npmjs.org/solid-ui/-/solid-ui-0.12.0.tgz", + "integrity": "sha512-ImiLiBE0SZP61kZ5dWweDRKQPxHYTFI8Jmxsz4yQIyPVcQB9SGM0Tdchz4zmSDRPvuw8GRFsC3USIHwIuvxj2g==", "requires": { "escape-html": "^1.0.3", + "mime-types": "^2.1.20", "node-uuid": "^1.4.7", - "rdflib": ">=0.19.0", - "regenerate": "^1.3.2", - "solid-auth-client": "^2.2.6", - "solid-auth-tls": "^0.1.2" + "rdflib": "^0.19.1", + "solid-auth-client": "^2.2.13", + "solid-auth-tls": "^0.1.2", + "solid-namespace": "0.2.0" }, "dependencies": { "node-uuid": { @@ -8484,17 +8638,10 @@ "xmldom": "^0.1.22" } }, - "solid-auth-client": { - "version": "2.2.11", - "resolved": "https://registry.npmjs.org/solid-auth-client/-/solid-auth-client-2.2.11.tgz", - "integrity": "sha512-5x8K32OueXo5NPBrUasAbInObRJuSOmYOaTQzmqjRl+sF3U5OLrJASDNaWM3OzUkwqxPLI9mkp+xaBwaMS7yZg==", - "requires": { - "@babel/runtime": "^7.0.0", - "@solid/oidc-rp": "^0.8.0", - "auth-header": "^1.0.0", - "commander": "^2.11.0", - "isomorphic-fetch": "^2.2.1" - } + "solid-namespace": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/solid-namespace/-/solid-namespace-0.2.0.tgz", + "integrity": "sha512-yQGQlTNDVtcMfzLz7OwL6Z8lJy9rN1ep0MgX28DPcja0DtA5pu1MzTpBVD9kvXl9X6eSEX73I7IYt0cUim0DrA==" } } }, @@ -8510,9 +8657,9 @@ } }, "source-map": { - "version": "0.5.7", - "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.5.7.tgz", - "integrity": "sha1-igOdLRAh0i0eoUyA2OpGi6LvP8w=" + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==" }, "source-map-resolve": { "version": "0.5.2", @@ -8534,14 +8681,6 @@ "requires": { "buffer-from": "^1.0.0", "source-map": "^0.6.0" - }, - "dependencies": { - "source-map": { - "version": "0.6.1", - "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", - "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", - "dev": true - } } }, "source-map-url": { @@ -8549,6 +8688,19 @@ "resolved": "https://registry.npmjs.org/source-map-url/-/source-map-url-0.4.0.tgz", "integrity": "sha1-PpNdfd1zYxuXZZlW1VEo6HtQhKM=" }, + "source-pane": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/source-pane/-/source-pane-1.0.3.tgz", + "integrity": "sha512-wmIQAf3o290d784vrhWydHECIYpXRwX3wJlI0doxgX8mAnNs77qmqi+bIjWhQaGFbg3QjkwbAhxuVjRkHITsQg==", + "requires": { + "babel-preset-env": "^1.6.1", + "babel-preset-metalab": "^1.0.0", + "mime-types": "^2.1.13", + "pane-registry": ">=1.0.0", + "rdflib": ">=0.17.0", + "solid-ui": ">=0.11.3" + } + }, "spawn-sync": { "version": "1.0.15", "resolved": "https://registry.npmjs.org/spawn-sync/-/spawn-sync-1.0.15.tgz", @@ -8573,9 +8725,9 @@ "dev": true }, "sshpk": { - "version": "1.14.2", - "resolved": "https://registry.npmjs.org/sshpk/-/sshpk-1.14.2.tgz", - "integrity": "sha1-xvxhZIo9nE52T9P8306hBeSSupg=", + "version": "1.15.2", + "resolved": "https://registry.npmjs.org/sshpk/-/sshpk-1.15.2.tgz", + "integrity": "sha512-Ra/OXQtuh0/enyl4ETZAfTaeksa6BXks5ZcjpSUNrjBr0DvrJKX+1fsKDPpT9TBXgHAFsa4510aNVgI8g/+SzA==", "requires": { "asn1": "~0.2.3", "assert-plus": "^1.0.0", @@ -8897,9 +9049,9 @@ }, "dependencies": { "debug": { - "version": "3.2.5", - "resolved": "https://registry.npmjs.org/debug/-/debug-3.2.5.tgz", - "integrity": "sha512-D61LaDQPQkxJ5AUM2mbSJRbPkNs/TmdmOeLAi1hgDkpDfIfetSrjmWhccwtuResSwMbACjx/xXQofvM9CE/aeg==", + "version": "3.2.6", + "resolved": "https://registry.npmjs.org/debug/-/debug-3.2.6.tgz", + "integrity": "sha512-mel+jf7nrtEl5Pn1Qx46zARXKDpBbvzezse7p7LqINmdoIk8PYP5SySaxEmYv6TZ0JyEKA1hsCId6DIhgITtWQ==", "dev": true, "requires": { "ms": "^2.1.1" @@ -9121,11 +9273,11 @@ "integrity": "sha1-DdTJ/6q8NXlgsbckEV1+Doai4fU=" }, "through2": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/through2/-/through2-2.0.3.tgz", - "integrity": "sha1-AARWmzfHx0ujnEPzzteNGtlBQL4=", + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/through2/-/through2-2.0.5.tgz", + "integrity": "sha512-/mrRod8xqpA+IHSLyGCQ2s8SPHiCDEeQJSep1jqLYeEUClOFG2Qsh+4FU6G9VeqpZnGW/Su8LQGc4YKni5rYSQ==", "requires": { - "readable-stream": "^2.1.5", + "readable-stream": "~2.3.6", "xtend": "~4.0.1" }, "dependencies": { @@ -9287,9 +9439,9 @@ "integrity": "sha512-C3TaO7K81YvjCgQH9Q1S3R3P3BtN3RIM8n+OvX4il1K1zgE8ZhI0op7kClgkxtutIE8hQrcrHBXvIheqKUUCxw==" }, "tunnel": { - "version": "0.0.5", - "resolved": "https://registry.npmjs.org/tunnel/-/tunnel-0.0.5.tgz", - "integrity": "sha512-gj5sdqherx4VZKMcBA4vewER7zdK25Td+z1npBqpbDys4eJrLx+SlYjJvq1bDXs2irkuJM5pf8ktaEQVipkrbA==" + "version": "0.0.6", + "resolved": "https://registry.npmjs.org/tunnel/-/tunnel-0.0.6.tgz", + "integrity": "sha512-1h/Lnq9yajKY2PEbBadPXj3VxsDDu844OnaAo52UVmIzIvwwtBPIuNvkjuzBlTWpfJyUbG3ez0KSBibQkj4ojg==" }, "tunnel-agent": { "version": "0.6.0", @@ -9319,8 +9471,7 @@ "tweetnacl": { "version": "0.14.5", "resolved": "https://registry.npmjs.org/tweetnacl/-/tweetnacl-0.14.5.tgz", - "integrity": "sha1-WuaBd/GS1EViadEIr6k/+HQ/T2Q=", - "optional": true + "integrity": "sha1-WuaBd/GS1EViadEIr6k/+HQ/T2Q=" }, "type-check": { "version": "0.3.2", @@ -9366,12 +9517,6 @@ "resolved": "https://registry.npmjs.org/commander/-/commander-2.17.1.tgz", "integrity": "sha512-wPMUt6FnH2yzG95SA6mzjQOEKUU3aLaDEmzs1ti+1E9h+CsrZghRlqEM/EJ4KscsQVG8uNN4uVreUeT8+drlgg==", "optional": true - }, - "source-map": { - "version": "0.6.1", - "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", - "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", - "optional": true } } }, @@ -9562,6 +9707,14 @@ } } }, + "uri-js": { + "version": "4.2.2", + "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.2.2.tgz", + "integrity": "sha512-KY9Frmirql91X2Qgjry0Wd4Y+YTdrdZheS8TFwvkbLWf/G5KNJDCh6pKL5OZctEW4+0Baa5idK2ZQuELRwPznQ==", + "requires": { + "punycode": "^2.1.0" + } + }, "urijs": { "version": "1.19.1", "resolved": "https://registry.npmjs.org/urijs/-/urijs-1.19.1.tgz", @@ -9885,15 +10038,9 @@ "integrity": "sha1-bRX7qITAhnnA136I53WegR4H+kE=", "dev": true }, - "yallist": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/yallist/-/yallist-2.1.2.tgz", - "integrity": "sha1-HBH5IY8HYImkfdUS+TxmmaaoHVI=", - "dev": true - }, "yargs": { "version": "3.32.0", - "resolved": "https://registry.npmjs.org/yargs/-/yargs-3.32.0.tgz", + "resolved": "http://registry.npmjs.org/yargs/-/yargs-3.32.0.tgz", "integrity": "sha1-AwiOnr+edWtpdRYR0qXvWRSCyZU=", "dev": true, "requires": { diff --git a/package.json b/package.json index 9f3805f9e..678ca416a 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "solid-server", "description": "Solid server on top of the file-system", - "version": "4.4.2", + "version": "5.0.0-beta.11", "author": { "name": "Tim Berners-Lee", "email": "timbl@w3.org" @@ -40,6 +40,11 @@ { "name": "Dmitri Zagidulin", "url": "https://github.com/dmitrizagidulin/" + }, + { + "name": "Arne Hassel", + "email": "arne.hassel@inrupt.com", + "url": "https://icanhasweb.net/" } ], "license": "MIT", @@ -51,11 +56,13 @@ "bugs": "https://github.com/solid/node-solid-server/issues", "dependencies": { "@solid/oidc-auth-manager": "^0.17.1", + "@solid/acl-check": "^0.2.0", "body-parser": "^1.18.3", "bootstrap": "^3.3.7", "busboy": "^0.2.12", "cached-path-relative": "^1.0.2", "camelize": "^1.0.0", + "cheerio": "^1.0.0-rc.2", "colorette": "^1.0.5", "commander": "^2.9.0", "cors": "^2.7.1", @@ -75,7 +82,7 @@ "ip-range-check": "0.0.2", "is-ip": "^2.0.0", "li": "^1.0.1", - "mashlib": "^0.7.15", + "mashlib": "^0.7.18", "mime-types": "^2.1.11", "negotiator": "^0.6.0", "node-fetch": "^2.1.2", @@ -83,12 +90,12 @@ "nodemailer": "^3.1.4", "oidc-op-express": "^0.0.3", "owasp-password-strength-test": "^1.3.0", + "proper-lockfile": "^3.2.0", "rdflib": "^0.17.1", "recursive-readdir": "^2.1.0", "rimraf": "^2.5.0", "solid-auth-client": "^2.2.12", "solid-namespace": "^0.1.0", - "solid-permissions": "^0.6.0", "solid-ws": "^0.2.3", "text-encoder-lite": "^1.0.1", "the-big-username-blacklist": "^1.5.1", @@ -129,7 +136,8 @@ "nyc": "cross-env NODE_TLS_REJECT_UNAUTHORIZED=0 nyc --reporter=text-summary mocha", "mocha": "cross-env NODE_TLS_REJECT_UNAUTHORIZED=0 mocha", "test": "npm run standard && npm run validate && npm run nyc", - "clean": "rimraf config/templates config/views" + "clean": "rimraf config/templates config/views", + "reset": "rimraf .db data && npm run clean" }, "nyc": { "reporter": [ diff --git a/test/integration/account-creation-oidc-test.js b/test/integration/account-creation-oidc-test.js index 9632b93a8..6d2be533d 100644 --- a/test/integration/account-creation-oidc-test.js +++ b/test/integration/account-creation-oidc-test.js @@ -80,21 +80,21 @@ describe('AccountManager (OIDC account creation tests)', function () { }) it('should not create WebID if no username is given', (done) => { - let subdomain = supertest('https://nicola.' + host) + let subdomain = supertest('https://' + host) subdomain.post('/api/accounts/new') .send('username=&password=12345') .expect(400, done) }) it('should not create WebID if no password is given', (done) => { - let subdomain = supertest('https://nicola.' + host) + let subdomain = supertest('https://' + host) subdomain.post('/api/accounts/new') .send('username=nicola&password=') .expect(400, done) }) it('should not create a WebID if it already exists', function (done) { - var subdomain = supertest('https://nicola.' + host) + var subdomain = supertest('https://' + host) subdomain.post('/api/accounts/new') .send('username=nicola&password=12345&acceptToc=true') .expect(302) @@ -112,14 +112,14 @@ describe('AccountManager (OIDC account creation tests)', function () { }) it('should not create WebID if T&C is not accepted', (done) => { - let subdomain = supertest('https://nicola.' + host) + let subdomain = supertest('https://' + host) subdomain.post('/api/accounts/new') .send('username=nicola&password=12345&acceptToc=') .expect(400, done) }) it('should create the default folders', function (done) { - var subdomain = supertest('https://nicola.' + host) + var subdomain = supertest('https://' + host) subdomain.post('/api/accounts/new') .send('username=nicola&password=12345&acceptToc=true') .expect(302) @@ -129,9 +129,9 @@ describe('AccountManager (OIDC account creation tests)', function () { } var domain = host.split(':')[0] var card = read(path.join('accounts/nicola.' + domain, - 'profile/card')) + 'profile/card$.ttl')) var cardAcl = read(path.join('accounts/nicola.' + domain, - 'profile/card.acl')) + 'profile/.acl')) var prefs = read(path.join('accounts/nicola.' + domain, 'settings/prefs.ttl')) var inboxAcl = read(path.join('accounts/nicola.' + domain, @@ -150,14 +150,15 @@ describe('AccountManager (OIDC account creation tests)', function () { }).timeout(20000) it('should link WebID to the root account', function (done) { - var subdomain = supertest('https://nicola.' + host) - subdomain.post('/api/accounts/new') + const domain = supertest('https://' + host) + domain.post('/api/accounts/new') .send('username=nicola&password=12345&acceptToc=true') .expect(302) .end(function (err) { if (err) { return done(err) } + const subdomain = supertest('https://nicola.' + host) subdomain.get('/.meta') .expect(200) .end(function (err, data) { @@ -183,31 +184,40 @@ describe('AccountManager (OIDC account creation tests)', function () { }) }).timeout(20000) - it('should create a private settings container', function (done) { - var subdomain = supertest('https://nicola.' + host) - subdomain.head('/settings/') - .expect(401) - .end(function (err) { - done(err) - }) - }) + describe('after setting up account', () => { + beforeEach(done => { + var subdomain = supertest('https://' + host) + subdomain.post('/api/accounts/new') + .send('username=nicola&password=12345&acceptToc=true') + .end(done) + }) - it('should create a private prefs file in the settings container', function (done) { - var subdomain = supertest('https://nicola.' + host) - subdomain.head('/inbox/prefs.ttl') - .expect(401) - .end(function (err) { - done(err) - }) - }) + it('should create a private settings container', function (done) { + var subdomain = supertest('https://nicola.' + host) + subdomain.head('/settings/') + .expect(401) + .end(function (err) { + done(err) + }) + }) - it('should create a private inbox container', function (done) { - var subdomain = supertest('https://nicola.' + host) - subdomain.head('/inbox/') - .expect(401) - .end(function (err) { - done(err) - }) + it('should create a private prefs file in the settings container', function (done) { + var subdomain = supertest('https://nicola.' + host) + subdomain.head('/inbox/prefs.ttl') + .expect(401) + .end(function (err) { + done(err) + }) + }) + + it('should create a private inbox container', function (done) { + var subdomain = supertest('https://nicola.' + host) + subdomain.head('/inbox/') + .expect(401) + .end(function (err) { + done(err) + }) + }) }) }) }) @@ -215,7 +225,7 @@ describe('AccountManager (OIDC account creation tests)', function () { describe('Single User signup page', () => { const serverUri = 'https://localhost:7457' const port = 7457 - var ldpHttpsServer + let ldpHttpsServer const rootDir = path.join(__dirname, '../resources/accounts/single-user/') const configPath = path.join(__dirname, '../resources/config') const ldp = ldnode.createServer({ @@ -231,7 +241,9 @@ describe('Single User signup page', () => { const server = supertest(serverUri) before(function (done) { - ldpHttpsServer = ldp.listen(port, done) + ldpHttpsServer = ldp.listen(port, () => server.post('/api/accounts/new') + .send('username=foo&password=12345&acceptToc=true') + .end(done)) }) after(function () { @@ -239,10 +251,10 @@ describe('Single User signup page', () => { fs.removeSync(rootDir) }) - it('should return a 401 unauthorized without accept text/html', done => { + it.skip('should return a 406 not acceptable without accept text/html', done => { server.get('/') .set('accept', 'text/plain') - .expect(401) + .expect(406) .end(done) }) }) @@ -283,7 +295,7 @@ describe('Signup page where Terms & Conditions are not being enforced', () => { }) it('should not enforce T&C upon creating account', function (done) { - var subdomain = supertest('https://nicola.' + host) + var subdomain = supertest('https://' + host) subdomain.post('/api/accounts/new') .send('username=nicola&password=12345') .expect(302, done) diff --git a/test/integration/account-manager-test.js b/test/integration/account-manager-test.js index 215e21339..4ebce7552 100644 --- a/test/integration/account-manager-test.js +++ b/test/integration/account-manager-test.js @@ -9,6 +9,7 @@ chai.should() const LDP = require('../../lib/ldp') const SolidHost = require('../../lib/models/solid-host') const AccountManager = require('../../lib/models/account-manager') +const ResourceMapper = require('../../lib/resource-mapper') const testAccountsDir = path.join(__dirname, '../resources/accounts') const accountTemplatePath = path.join(__dirname, '../../default-templates/new-account') @@ -29,7 +30,12 @@ describe('AccountManager', () => { describe('in multi user mode', () => { let multiuser = true - let store = new LDP({ root: testAccountsDir, multiuser }) + let resourceMapper = new ResourceMapper({ + rootUrl: 'https://localhost:8443/', + rootPath: process.cwd(), + includeHost: multiuser + }) + let store = new LDP({ multiuser, resourceMapper }) let options = { multiuser, store, host } let accountManager = AccountManager.from(options) @@ -54,9 +60,14 @@ describe('AccountManager', () => { let multiuser = false it('resolves to true if root .acl exists in root storage', () => { + let resourceMapper = new ResourceMapper({ + rootUrl: 'https://localhost:8443/', + includeHost: multiuser, + rootPath: path.join(testAccountsDir, 'tim.localhost') + }) let store = new LDP({ - root: path.join(testAccountsDir, 'tim.localhost'), - multiuser + multiuser, + resourceMapper }) let options = { multiuser, store, host } let accountManager = AccountManager.from(options) @@ -68,9 +79,14 @@ describe('AccountManager', () => { }) it('resolves to false if root .acl does not exist in root storage', () => { + let resourceMapper = new ResourceMapper({ + rootUrl: 'https://localhost:8443/', + includeHost: multiuser, + rootPath: testAccountsDir + }) let store = new LDP({ - root: testAccountsDir, - multiuser + multiuser, + resourceMapper }) let options = { multiuser, store, host } let accountManager = AccountManager.from(options) @@ -86,7 +102,12 @@ describe('AccountManager', () => { describe('createAccountFor()', () => { it('should create an account directory', () => { let multiuser = true - let store = new LDP({ root: testAccountsDir, multiuser }) + let resourceMapper = new ResourceMapper({ + rootUrl: 'https://localhost:8443/', + includeHost: multiuser, + rootPath: testAccountsDir + }) + let store = new LDP({ multiuser, resourceMapper }) let options = { host, multiuser, store, accountTemplatePath } let accountManager = AccountManager.from(options) @@ -107,7 +128,7 @@ describe('AccountManager', () => { expect(found).to.be.true }) .then(() => { - let profile = fs.readFileSync(path.join(accountDir, '/profile/card'), 'utf8') + let profile = fs.readFileSync(path.join(accountDir, '/profile/card$.ttl'), 'utf8') expect(profile).to.include('"Alice Q."') let rootAcl = fs.readFileSync(path.join(accountDir, '.acl'), 'utf8') diff --git a/test/integration/account-template-test.js b/test/integration/account-template-test.js index ff5cdcc94..930b97d28 100644 --- a/test/integration/account-template-test.js +++ b/test/integration/account-template-test.js @@ -46,7 +46,7 @@ describe('AccountTemplate', () => { return template.processAccount(accountPath) }) .then(() => { - let profile = fs.readFileSync(path.join(accountPath, '/profile/card'), 'utf8') + let profile = fs.readFileSync(path.join(accountPath, '/profile/card$.ttl'), 'utf8') expect(profile).to.include('"Alice Q."') let rootAcl = fs.readFileSync(path.join(accountPath, '.acl'), 'utf8') diff --git a/test/integration/acl-oidc-test.js b/test/integration/acl-oidc-test.js index 336fe2a70..80fa600e4 100644 --- a/test/integration/acl-oidc-test.js +++ b/test/integration/acl-oidc-test.js @@ -30,16 +30,15 @@ let userCredentials = { } function issueIdToken (oidcProvider, webId) { - return Promise.resolve() - .then(() => { - let jwt = IDToken.issue(oidcProvider, { - sub: webId, - aud: [ serverUri, 'client123' ], - azp: 'client123' - }) - - return jwt.encode() + return Promise.resolve().then(() => { + let jwt = IDToken.issue(oidcProvider, { + sub: webId, + aud: [serverUri, 'client123'], + azp: 'client123' }) + + return jwt.encode() + }) } const argv = { @@ -65,23 +64,19 @@ describe('ACL with WebID+OIDC over HTTP', function () { before(done => { ldp = ldnode.createServer(argv) - loadProvider(oidcProviderPath) - .then(provider => { - oidcProvider = provider - - return Promise.all([ - issueIdToken(oidcProvider, user1), - issueIdToken(oidcProvider, user2) - ]) - }) - .then(tokens => { - userCredentials.user1 = tokens[0] - userCredentials.user2 = tokens[1] - }) - .then(() => { - ldpHttpsServer = ldp.listen(port, done) - }) - .catch(console.error) + loadProvider(oidcProviderPath).then(provider => { + oidcProvider = provider + + return Promise.all([ + issueIdToken(oidcProvider, user1), + issueIdToken(oidcProvider, user2) + ]) + }).then(tokens => { + userCredentials.user1 = tokens[0] + userCredentials.user2 = tokens[1] + }).then(() => { + ldpHttpsServer = ldp.listen(port, done) + }).catch(console.error) }) after(() => { @@ -92,11 +87,12 @@ describe('ACL with WebID+OIDC over HTTP', function () { const origin1 = 'http://example.org/' const origin2 = 'http://example.com/' - function createOptions (path, user) { + function createOptions (path, user, contentType = 'text/plain') { const options = { url: timAccountUri + path, headers: { - accept: 'text/turtle' + 'accept': 'text/turtle', + 'content-type': contentType } } if (user) { @@ -108,26 +104,26 @@ describe('ACL with WebID+OIDC over HTTP', function () { } describe('no ACL', function () { - it('should return 403 for any resource', function (done) { - var options = createOptions('/no-acl/', 'user1') - request(options, function (error, response, body) { - assert.equal(error, null) - assert.equal(response.statusCode, 403) - done() - }) - }) - it('should not have the `User` set in the Response Header', function (done) { + it('Should return 500 since no ACL is a server misconfig', function (done) { var options = createOptions('/no-acl/', 'user1') request(options, function (error, response, body) { assert.equal(error, null) - assert.notProperty(response.headers, 'user') + assert.equal(response.statusCode, 500) done() }) }) + // it('should not have the `User` set in the Response Header', function (done) { + // var options = createOptions('/no-acl/', 'user1') + // request(options, function (error, response, body) { + // assert.equal(error, null) + // assert.notProperty(response.headers, 'user') + // done() + // }) + // }) }) describe('empty .acl', function () { - describe('with no defaultForNew in parent path', function () { + describe('with no default in parent path', function () { it('should give no access', function (done) { var options = createOptions('/empty-acl/test-folder', 'user1') options.body = '' @@ -155,7 +151,7 @@ describe('ACL with WebID+OIDC over HTTP', function () { }) }) }) - describe('with defaultForNew in parent path', function () { + describe('with default in parent path', function () { before(function () { rm('/accounts-acl/tim.localhost/write-acl/empty-acl/another-empty-folder/test-file.acl') rm('/accounts-acl/tim.localhost/write-acl/empty-acl/test-folder/test-file') @@ -169,34 +165,34 @@ describe('ACL with WebID+OIDC over HTTP', function () { options.body = '' request.put(options, function (error, response, body) { assert.equal(error, null) - assert.equal(response.statusCode, 409) + assert.equal(response.statusCode, 403) // TODO - why should this be a 409? done() }) }) - it('should allow creation of new files', function (done) { + it('should fail creation of new files', function (done) { var options = createOptions('/write-acl/empty-acl/test-file', 'user1') options.body = '' request.put(options, function (error, response, body) { assert.equal(error, null) - assert.equal(response.statusCode, 201) + assert.equal(response.statusCode, 403) done() }) }) - it('should allow creation of new files in deeper paths', function (done) { + it('should fail creation of new files in deeper paths', function (done) { var options = createOptions('/write-acl/empty-acl/test-folder/test-file', 'user1') options.body = '' request.put(options, function (error, response, body) { assert.equal(error, null) - assert.equal(response.statusCode, 201) + assert.equal(response.statusCode, 403) done() }) }) - it('Should create empty acl file', function (done) { + it('Should not create empty acl file', function (done) { var options = createOptions('/write-acl/empty-acl/another-empty-folder/test-file.acl', 'user1') options.body = '' request.put(options, function (error, response, body) { assert.equal(error, null) - assert.equal(response.statusCode, 201) + assert.equal(response.statusCode, 403) done() }) }) @@ -209,6 +205,14 @@ describe('ACL with WebID+OIDC over HTTP', function () { done() }) }) + it('should fail as acl:default it used to try to authorize', function (done) { + var options = createOptions('/write-acl/bad-acl-access/.acl', 'user1') + request.get(options, function (error, response, body) { + assert.equal(error, null) + assert.equal(response.statusCode, 403) + done() + }) + }) it('should create test file', function (done) { var options = createOptions('/write-acl/test-file', 'user1') options.body = ' .' @@ -218,8 +222,8 @@ describe('ACL with WebID+OIDC over HTTP', function () { done() }) }) - it("should create test file's acl file", function (done) { - var options = createOptions('/write-acl/test-file.acl', 'user1') + it('should create test file\'s acl file', function (done) { + var options = createOptions('/write-acl/test-file.acl', 'user1', 'text/turtle') options.body = '' request.put(options, function (error, response, body) { assert.equal(error, null) @@ -227,12 +231,11 @@ describe('ACL with WebID+OIDC over HTTP', function () { done() }) }) - it("should access test file's acl file", function (done) { + it('should not access test file\'s new empty acl file', function (done) { var options = createOptions('/write-acl/test-file.acl', 'user1') request.get(options, function (error, response, body) { assert.equal(error, null) - assert.equal(response.statusCode, 200) - assert.match(response.headers['content-type'], /text\/turtle/) + assert.equal(response.statusCode, 403) done() }) }) @@ -253,7 +256,7 @@ describe('ACL with WebID+OIDC over HTTP', function () { }) it('should PUT new ACL file', function (done) { - var options = createOptions('/origin/test-folder/.acl', 'user1') + var options = createOptions('/origin/test-folder/.acl', 'user1', 'text/turtle') options.body = '<#Owner> a ;\n' + ' ;\n' + ' <' + user1 + '>;\n' + @@ -263,13 +266,19 @@ describe('ACL with WebID+OIDC over HTTP', function () { ' <./>;\n' + ' ;\n' + ' <' + origin1 + '>;\n' + - ' .\n' + ' .\n' + + '<#Somebody> a ;\n' + + ' <./>;\n' + + ' <' + user2 + '>;\n' + + ' <./>;\n' + + ' <' + origin1 + '>;\n' + + ' .\n' request.put(options, function (error, response, body) { assert.equal(error, null) assert.equal(response.statusCode, 201) done() - // TODO triple header - // TODO user header + // TODO triple header + // TODO user header }) }) it('user1 should be able to access test directory', function (done) { @@ -282,28 +291,36 @@ describe('ACL with WebID+OIDC over HTTP', function () { done() }) }) - it('user1 should be able to access to test directory when origin is valid', - function (done) { - var options = createOptions('/origin/test-folder/', 'user1') - options.headers.origin = origin1 + it('user2 should be able to access public test directory with wrong origin', function (done) { + var options = createOptions('/origin/test-folder/', 'user2') + options.headers.origin = origin2 - request.head(options, function (error, response, body) { - assert.equal(error, null) - assert.equal(response.statusCode, 200) - done() - }) + request.head(options, function (error, response, body) { + assert.equal(error, null) + assert.equal(response.statusCode, 200) + done() }) - it('user1 should be able to access test directory when origin is invalid', - function (done) { - var options = createOptions('/origin/test-folder/', 'user1') - options.headers.origin = origin2 + }) + it('user1 should be able to access to test directory when origin is valid', function (done) { + var options = createOptions('/origin/test-folder/', 'user1') + options.headers.origin = origin1 - request.head(options, function (error, response, body) { - assert.equal(error, null) - assert.equal(response.statusCode, 200) - done() - }) + request.head(options, function (error, response, body) { + assert.equal(error, null) + assert.equal(response.statusCode, 200) + done() + }) + }) + it('user1 should be able to access public test directory even when origin is invalid', function (done) { + var options = createOptions('/origin/test-folder/', 'user1') + options.headers.origin = origin2 + + request.head(options, function (error, response, body) { + assert.equal(error, null) + assert.equal(response.statusCode, 200) + done() }) + }) it('agent should be able to access test directory', function (done) { var options = createOptions('/origin/test-folder/') options.headers.origin = origin1 @@ -314,31 +331,52 @@ describe('ACL with WebID+OIDC over HTTP', function () { done() }) }) - it('agent should be able to access to test directory when origin is valid', - function (done) { - var options = createOptions('/origin/test-folder/', 'user1') - options.headers.origin = origin1 + it('agent should be able to access to test directory when origin is valid', function (done) { + var options = createOptions('/origin/test-folder/', 'user1') + options.headers.origin = origin1 - request.head(options, function (error, response, body) { - assert.equal(error, null) - assert.equal(response.statusCode, 200) - done() - }) + request.head(options, function (error, response, body) { + assert.equal(error, null) + assert.equal(response.statusCode, 200) + done() }) - it('agent should be able to access test directory when origin is invalid', - function (done) { - var options = createOptions('/origin/test-folder/') - options.headers.origin = origin2 + }) + it('agent should be able to access public test directory even when origin is invalid', function (done) { + var options = createOptions('/origin/test-folder/') + options.headers.origin = origin2 - request.head(options, function (error, response, body) { - assert.equal(error, null) - assert.equal(response.statusCode, 200) - done() - }) + request.head(options, function (error, response, body) { + assert.equal(error, null) + assert.equal(response.statusCode, 200) + done() + }) + }) + it('user2 should be able to write to test directory with correct origin', function (done) { + var options = createOptions('/origin/test-folder/test1.txt', 'user2', 'text/plain') + options.headers.origin = origin1 + options.body = 'DAAAAAHUUUT' + request.put(options, function (error, response, body) { + assert.equal(error, null) + assert.equal(response.statusCode, 201) + done() + }) + }) + it('user2 should not be able to write to test directory with wrong origin', function (done) { + var options = createOptions('/origin/test-folder/test2.txt', 'user2', 'text/plain') + options.headers.origin = origin2 + options.body = 'ARRRRGH' + request.put(options, function (error, response, body) { + assert.equal(error, null) + assert.equal(response.statusCode, 403) + assert.equal(response.statusMessage, 'Origin Unauthorized') + done() }) + }) after(function () { rm('/accounts-acl/tim.localhost/origin/test-folder/.acl') + rm('/accounts-acl/tim.localhost/origin/test-folder/test1.txt') + rm('/accounts-acl/tim.localhost/origin/test-folder/test2.txt') }) }) @@ -361,7 +399,7 @@ describe('ACL with WebID+OIDC over HTTP', function () { }) }) it('user1 should be able to modify ACL file', function (done) { - var options = createOptions('/read-acl/.acl', 'user1') + var options = createOptions('/read-acl/.acl', 'user1', 'text/turtle') options.body = body request.put(options, function (error, response, body) { assert.equal(error, null) @@ -382,15 +420,17 @@ describe('ACL with WebID+OIDC over HTTP', function () { request.head(options, function (error, response, body) { assert.equal(error, null) assert.equal(response.statusCode, 403) + assert.equal(response.statusMessage, 'User Unauthorized') done() }) }) it('user2 should not be able to modify ACL file', function (done) { - var options = createOptions('/read-acl/.acl', 'user2') + var options = createOptions('/read-acl/.acl', 'user2', 'text/turtle') options.body = ' .' request.put(options, function (error, response, body) { assert.equal(error, null) assert.equal(response.statusCode, 403) + assert.equal(response.statusMessage, 'User Unauthorized') done() }) }) @@ -403,11 +443,46 @@ describe('ACL with WebID+OIDC over HTTP', function () { }) }) it('agent should not be able to modify ACL file', function (done) { - var options = createOptions('/read-acl/.acl') + var options = createOptions('/read-acl/.acl', null, 'text/turtle') options.body = ' .' request.put(options, function (error, response, body) { assert.equal(error, null) assert.equal(response.statusCode, 401) + assert.equal(response.statusMessage, 'Unauthenticated') + done() + }) + }) + // Deep acl:accessTo inheritance is not supported yet #963 + it.skip('user1 should be able to access deep test directory ACL', function (done) { + var options = createOptions('/read-acl/deeper-tree/.acl', 'user1') + request.head(options, function (error, response, body) { + assert.equal(error, null) + assert.equal(response.statusCode, 200) + done() + }) + }) + it.skip('user1 should not be able to access deep test dir', function (done) { + var options = createOptions('/read-acl/deeper-tree/', 'user1') + request.head(options, function (error, response, body) { + assert.equal(error, null) + assert.equal(response.statusCode, 403) + assert.equal(response.statusMessage, 'User Unauthorized') + done() + }) + }) + it.skip('user1 should able to access even deeper test directory', function (done) { + var options = createOptions('/read-acl/deeper-tree/acls-only-on-top/', 'user1') + request.head(options, function (error, response, body) { + assert.equal(error, null) + assert.equal(response.statusCode, 200) + done() + }) + }) + it.skip('user1 should able to access even deeper test file', function (done) { + var options = createOptions('/read-acl/deeper-tree/acls-only-on-top/example.ttl', 'user1') + request.head(options, function (error, response, body) { + assert.equal(error, null) + assert.equal(response.statusCode, 200) done() }) }) @@ -415,7 +490,7 @@ describe('ACL with WebID+OIDC over HTTP', function () { describe('Append-only', function () { // var body = fs.readFileSync(__dirname + '/resources/append-acl/abc.ttl.acl') - it("user1 should be able to access test file's ACL file", function (done) { + it('user1 should be able to access test file\'s ACL file', function (done) { var options = createOptions('/append-acl/abc.ttl.acl', 'user1') request.head(options, function (error, response) { assert.equal(error, null) @@ -423,7 +498,7 @@ describe('ACL with WebID+OIDC over HTTP', function () { done() }) }) - it('user1 should be able to PATCH a resource', function (done) { + it.skip('user1 should be able to PATCH a resource', function (done) { var options = createOptions('/append-inherited/test.ttl', 'user1') options.body = 'INSERT DATA { :test :hello 456 .}' options.headers['content-type'] = 'application/sparql-update' @@ -433,6 +508,16 @@ describe('ACL with WebID+OIDC over HTTP', function () { done() }) }) + it('user1 should be able to PATCH an existing resource', function (done) { + var options = createOptions('/append-inherited/test.ttl', 'user1') + options.body = 'INSERT DATA { :test :hello 789 .}' + options.headers['content-type'] = 'application/sparql-update' + request.patch(options, function (error, response, body) { + assert.equal(error, null) + assert.equal(response.statusCode, 200) + done() + }) + }) it('user1 should be able to access test file', function (done) { var options = createOptions('/append-acl/abc.ttl', 'user1') request.head(options, function (error, response, body) { @@ -443,7 +528,7 @@ describe('ACL with WebID+OIDC over HTTP', function () { }) // TODO POST instead of PUT it('user1 should be able to modify test file', function (done) { - var options = createOptions('/append-acl/abc.ttl', 'user1') + var options = createOptions('/append-acl/abc.ttl', 'user1', 'text/turtle') options.body = ' .\n' request.put(options, function (error, response, body) { assert.equal(error, null) @@ -451,28 +536,31 @@ describe('ACL with WebID+OIDC over HTTP', function () { done() }) }) - it("user2 should not be able to access test file's ACL file", function (done) { - var options = createOptions('/append-acl/abc.ttl.acl', 'user2') + it('user2 should not be able to access test file\'s ACL file', function (done) { + var options = createOptions('/append-acl/abc.ttl.acl', 'user2', 'text/turtle') request.head(options, function (error, response, body) { assert.equal(error, null) assert.equal(response.statusCode, 403) + assert.equal(response.statusMessage, 'User Unauthorized') done() }) }) it('user2 should not be able to access test file', function (done) { - var options = createOptions('/append-acl/abc.ttl', 'user2') + var options = createOptions('/append-acl/abc.ttl', 'user2', 'text/turtle') request.head(options, function (error, response, body) { assert.equal(error, null) assert.equal(response.statusCode, 403) + assert.equal(response.statusMessage, 'User Unauthorized') done() }) }) it('user2 (with append permission) cannot use PUT to append', function (done) { - var options = createOptions('/append-acl/abc.ttl', 'user2') + var options = createOptions('/append-acl/abc.ttl', 'user2', 'text/turtle') options.body = ' .\n' request.put(options, function (error, response, body) { assert.equal(error, null) assert.equal(response.statusCode, 403) + assert.equal(response.statusMessage, 'User Unauthorized') done() }) }) @@ -481,15 +569,17 @@ describe('ACL with WebID+OIDC over HTTP', function () { request.head(options, function (error, response, body) { assert.equal(error, null) assert.equal(response.statusCode, 401) + assert.equal(response.statusMessage, 'Unauthenticated') done() }) }) it('agent (with append permissions) should not PUT', function (done) { - var options = createOptions('/append-acl/abc.ttl') + var options = createOptions('/append-acl/abc.ttl', null, 'text/turtle') options.body = ' .\n' request.put(options, function (error, response, body) { assert.equal(error, null) assert.equal(response.statusCode, 401) + assert.equal(response.statusMessage, 'Unauthenticated') done() }) }) @@ -519,7 +609,7 @@ describe('ACL with WebID+OIDC over HTTP', function () { // done() // }) // }) - it.skip('user1 should be able to access test directory', function (done) { + it('user1 should be able to access test directory', function (done) { var options = createOptions('/group/test-folder/', 'user1') request.head(options, function (error, response, body) { @@ -528,7 +618,7 @@ describe('ACL with WebID+OIDC over HTTP', function () { done() }) }) - it.skip('user2 should be able to access test directory', function (done) { + it('user2 should be able to access test directory', function (done) { var options = createOptions('/group/test-folder/', 'user2') request.head(options, function (error, response, body) { @@ -537,19 +627,19 @@ describe('ACL with WebID+OIDC over HTTP', function () { done() }) }) - it.skip('user2 should be able to write a file in the test directory', - function (done) { - var options = createOptions('/group/test-folder/test.ttl', 'user2') - options.body = '<#Dahut> a .\n' + it('user2 should be able to write a file in the test directory', function (done) { + var options = createOptions('/group/test-folder/test.ttl', 'user2', 'text/turtle') + options.body = '<#Dahut> a .\n' - request.put(options, function (error, response, body) { - assert.equal(error, null) - assert.equal(response.statusCode, 201) - done() - }) + request.put(options, function (error, response, body) { + assert.equal(error, null) + assert.equal(response.statusCode, 201) + done() }) - it.skip('user1 should be able to get the file', function (done) { - var options = createOptions('/group/test-folder/test.ttl', 'user1') + }) + + it('user1 should be able to get the file', function (done) { + var options = createOptions('/group/test-folder/test.ttl', 'user1', 'text/turtle') request.get(options, function (error, response, body) { assert.equal(error, null) @@ -557,19 +647,20 @@ describe('ACL with WebID+OIDC over HTTP', function () { done() }) }) - it.skip('user2 should not be able to write to the ACL', - function (done) { - var options = createOptions('/group/test-folder/.acl', 'user2') - options.body = '<#Dahut> a .\n' + it('user2 should not be able to write to the ACL', function (done) { + var options = createOptions('/group/test-folder/.acl', 'user2', 'text/turtle') + options.body = '<#Dahut> a .\n' - request.put(options, function (error, response, body) { - assert.equal(error, null) - assert.equal(response.statusCode, 403) - done() - }) + request.put(options, function (error, response, body) { + assert.equal(error, null) + assert.equal(response.statusCode, 403) + assert.equal(response.statusMessage, 'User Unauthorized') + done() }) - it.skip('user1 should be able to delete the file', function (done) { - var options = createOptions('/group/test-folder/test.ttl', 'user1') + }) + + it('user1 should be able to delete the file', function (done) { + var options = createOptions('/group/test-folder/test.ttl', 'user1', 'text/turtle') request.delete(options, function (error, response, body) { assert.equal(error, null) @@ -577,26 +668,24 @@ describe('ACL with WebID+OIDC over HTTP', function () { done() }) }) - it.skip('We should have a 500 with invalid group listings', - function (done) { - var options = createOptions('/group/test-folder/some-other-file.txt', 'user2') + it('We should have a 500 with invalid group listings', function (done) { + var options = createOptions('/group/test-folder/some-other-file.txt', 'user2') - request.get(options, function (error, response, body) { - assert.equal(error, null) - assert.equal(response.statusCode, 500) - done() - }) + request.get(options, function (error, response, body) { + assert.equal(error, null) + assert.equal(response.statusCode, 500) + done() }) - it.skip('We should have a 404 for non-existent file', - function (done) { - var options = createOptions('/group/test-folder/nothere.txt', 'user2') + }) + it('We should have a 404 for non-existent file', function (done) { + var options = createOptions('/group/test-folder/nothere.txt', 'user2') - request.get(options, function (error, response, body) { - assert.equal(error, null) - assert.equal(response.statusCode, 404) - done() - }) + request.get(options, function (error, response, body) { + assert.equal(error, null) + assert.equal(response.statusCode, 404) + done() }) + }) }) describe('Restricted', function () { @@ -608,8 +697,8 @@ describe('ACL with WebID+OIDC over HTTP', function () { ' <./abc2.ttl>;\n' + ' <' + user2 + '>;\n' + ' , .\n' - it("user1 should be able to modify test file's ACL file", function (done) { - var options = createOptions('/append-acl/abc2.ttl.acl', 'user1') + it('user1 should be able to modify test file\'s ACL file', function (done) { + var options = createOptions('/append-acl/abc2.ttl.acl', 'user1', 'text/turtle') options.body = body request.put(options, function (error, response, body) { assert.equal(error, null) @@ -617,8 +706,8 @@ describe('ACL with WebID+OIDC over HTTP', function () { done() }) }) - it("user1 should be able to access test file's ACL file", function (done) { - var options = createOptions('/append-acl/abc2.ttl.acl', 'user1') + it('user1 should be able to access test file\'s ACL file', function (done) { + var options = createOptions('/append-acl/abc2.ttl.acl', 'user1', 'text/turtle') request.head(options, function (error, response, body) { assert.equal(error, null) assert.equal(response.statusCode, 200) @@ -626,7 +715,7 @@ describe('ACL with WebID+OIDC over HTTP', function () { }) }) it('user1 should be able to access test file', function (done) { - var options = createOptions('/append-acl/abc2.ttl', 'user1') + var options = createOptions('/append-acl/abc2.ttl', 'user1', 'text/turtle') request.head(options, function (error, response, body) { assert.equal(error, null) assert.equal(response.statusCode, 200) @@ -634,7 +723,7 @@ describe('ACL with WebID+OIDC over HTTP', function () { }) }) it('user1 should be able to modify test file', function (done) { - var options = createOptions('/append-acl/abc2.ttl', 'user1') + var options = createOptions('/append-acl/abc2.ttl', 'user1', 'text/turtle') options.body = ' .\n' request.put(options, function (error, response, body) { assert.equal(error, null) @@ -650,16 +739,17 @@ describe('ACL with WebID+OIDC over HTTP', function () { done() }) }) - it("user2 should not be able to access test file's ACL file", function (done) { + it('user2 should not be able to access test file\'s ACL file', function (done) { var options = createOptions('/append-acl/abc2.ttl.acl', 'user2') request.head(options, function (error, response, body) { assert.equal(error, null) assert.equal(response.statusCode, 403) + assert.equal(response.statusMessage, 'User Unauthorized') done() }) }) it('user2 should be able to modify test file', function (done) { - var options = createOptions('/append-acl/abc2.ttl', 'user2') + var options = createOptions('/append-acl/abc2.ttl', 'user2', 'text/turtle') options.body = ' .\n' request.put(options, function (error, response, body) { assert.equal(error, null) @@ -672,21 +762,23 @@ describe('ACL with WebID+OIDC over HTTP', function () { request.head(options, function (error, response, body) { assert.equal(error, null) assert.equal(response.statusCode, 401) + assert.equal(response.statusMessage, 'Unauthenticated') done() }) }) it('agent should not be able to modify test file', function (done) { - var options = createOptions('/append-acl/abc2.ttl') + var options = createOptions('/append-acl/abc2.ttl', null, 'text/turtle') options.body = ' .\n' request.put(options, function (error, response, body) { assert.equal(error, null) assert.equal(response.statusCode, 401) + assert.equal(response.statusMessage, 'Unauthenticated') done() }) }) }) - describe('defaultForNew', function () { + describe('default', function () { before(function () { rm('/accounts-acl/tim.localhost/write-acl/default-for-new/.acl') rm('/accounts-acl/tim.localhost/write-acl/default-for-new/test-file.ttl') @@ -695,15 +787,15 @@ describe('ACL with WebID+OIDC over HTTP', function () { var body = '<#Owner> a ;\n' + ' <./>;\n' + ' <' + user1 + '>;\n' + - ' <./>;\n' + + ' <./>;\n' + ' , , .\n' + '<#Default> a ;\n' + ' <./>;\n' + - ' <./>;\n' + + ' <./>;\n' + ' ;\n' + ' .\n' - it("user1 should be able to modify test directory's ACL file", function (done) { - var options = createOptions('/write-acl/default-for-new/.acl', 'user1') + it('user1 should be able to modify test directory\'s ACL file', function (done) { + var options = createOptions('/write-acl/default-for-new/.acl', 'user1', 'text/turtle') options.body = body request.put(options, function (error, response, body) { assert.equal(error, null) @@ -711,7 +803,7 @@ describe('ACL with WebID+OIDC over HTTP', function () { done() }) }) - it("user1 should be able to access test direcotory's ACL file", function (done) { + it('user1 should be able to access test direcotory\'s ACL file', function (done) { var options = createOptions('/write-acl/default-for-new/.acl', 'user1') request.head(options, function (error, response, body) { assert.equal(error, null) @@ -720,7 +812,7 @@ describe('ACL with WebID+OIDC over HTTP', function () { }) }) it('user1 should be able to create new test file', function (done) { - var options = createOptions('/write-acl/default-for-new/test-file.ttl', 'user1') + var options = createOptions('/write-acl/default-for-new/test-file.ttl', 'user1', 'text/turtle') options.body = ' .\n' request.put(options, function (error, response, body) { assert.equal(error, null) @@ -736,11 +828,12 @@ describe('ACL with WebID+OIDC over HTTP', function () { done() }) }) - it("user2 should not be able to access test direcotory's ACL file", function (done) { + it('user2 should not be able to access test direcotory\'s ACL file', function (done) { var options = createOptions('/write-acl/default-for-new/.acl', 'user2') request.head(options, function (error, response, body) { assert.equal(error, null) assert.equal(response.statusCode, 403) + assert.equal(response.statusMessage, 'User Unauthorized') done() }) }) @@ -753,11 +846,12 @@ describe('ACL with WebID+OIDC over HTTP', function () { }) }) it('user2 should not be able to modify new test file', function (done) { - var options = createOptions('/write-acl/default-for-new/test-file.ttl', 'user2') + var options = createOptions('/write-acl/default-for-new/test-file.ttl', 'user2', 'text/turtle') options.body = ' .\n' request.put(options, function (error, response, body) { assert.equal(error, null) assert.equal(response.statusCode, 403) + assert.equal(response.statusMessage, 'User Unauthorized') done() }) }) @@ -770,11 +864,12 @@ describe('ACL with WebID+OIDC over HTTP', function () { }) }) it('agent should not be able to modify new test file', function (done) { - var options = createOptions('/write-acl/default-for-new/test-file.ttl') + var options = createOptions('/write-acl/default-for-new/test-file.ttl', null, 'text/turtle') options.body = ' .\n' request.put(options, function (error, response, body) { assert.equal(error, null) assert.equal(response.statusCode, 401) + assert.equal(response.statusMessage, 'Unauthenticated') done() }) }) @@ -784,4 +879,15 @@ describe('ACL with WebID+OIDC over HTTP', function () { rm('/accounts-acl/tim.localhost/write-acl/default-for-new/test-file.ttl') }) }) + + describe('Wrongly set accessTo', function () { + it('user1 should be able to access test directory', function (done) { + var options = createOptions('/dot-acl/', 'user1') + request.head(options, function (error, response, body) { + assert.equal(error, null) + assert.equal(response.statusCode, 403) + done() + }) + }) + }) }) diff --git a/test/integration/acl-tls-test.js b/test/integration/acl-tls-test.js index aedae7289..c2103f669 100644 --- a/test/integration/acl-tls-test.js +++ b/test/integration/acl-tls-test.js @@ -77,7 +77,8 @@ describe('ACL with WebID+TLS', function () { var options = { url: address + path, headers: { - accept: 'text/turtle' + accept: 'text/turtle', + 'content-type': 'text/plain' } } if (user) { @@ -87,27 +88,28 @@ describe('ACL with WebID+TLS', function () { } describe('no ACL', function () { - it('should return 403 for any resource', function (done) { + it('should return 500 for any resource', function (done) { + rm('.acl') var options = createOptions('/acl-tls/no-acl/', 'user1') request(options, function (error, response, body) { assert.equal(error, null) - assert.equal(response.statusCode, 403) + assert.equal(response.statusCode, 500) done() }) }) it('should have `User` set in the Response Header', function (done) { + rm('.acl') var options = createOptions('/acl-tls/no-acl/', 'user1') request(options, function (error, response, body) { assert.equal(error, null) - assert.equal(response.statusCode, 403) - assert.equal(response.headers['user'], - 'https://user1.databox.me/profile/card#me') + assert.equal(response.headers['user'], 'https://user1.databox.me/profile/card#me') done() }) }) - it('should return a 401 and WWW-Authenticate header without credentials', (done) => { + it.skip('should return a 401 and WWW-Authenticate header without credentials', (done) => { + rm('.acl') let options = { url: address + '/acl-tls/no-acl/', headers: { accept: 'text/turtle' } @@ -116,15 +118,14 @@ describe('ACL with WebID+TLS', function () { request(options, (error, response, body) => { assert.equal(error, null) assert.equal(response.statusCode, 401) - assert.equal(response.headers['www-authenticate'], - 'WebID-TLS realm="https://localhost:8443"') + assert.equal(response.headers['www-authenticate'], 'WebID-TLS realm="https://localhost:8443"') done() }) }) }) describe('empty .acl', function () { - describe('with no defaultForNew in parent path', function () { + describe('with no default in parent path', function () { it('should give no access', function (done) { var options = createOptions('/acl-tls/empty-acl/test-folder', 'user1') options.body = '' @@ -158,7 +159,7 @@ describe('ACL with WebID+TLS', function () { }) }) }) - describe('with defaultForNew in parent path', function () { + describe('with default in parent path', function () { before(function () { rm('/acl-tls/write-acl/empty-acl/another-empty-folder/test-file.acl') rm('/acl-tls/write-acl/empty-acl/test-folder/test-file') @@ -172,29 +173,29 @@ describe('ACL with WebID+TLS', function () { options.body = '' request.put(options, function (error, response, body) { assert.equal(error, null) - assert.equal(response.statusCode, 409) + assert.equal(response.statusCode, 403) // TODO: SHOULD THIS RETURN A 409? done() }) }) - it('should allow creation of new files', function (done) { + it('should not allow creation of new files', function (done) { var options = createOptions('/acl-tls/write-acl/empty-acl/test-file', 'user1') options.body = '' request.put(options, function (error, response, body) { assert.equal(error, null) - assert.equal(response.statusCode, 201) + assert.equal(response.statusCode, 403) done() }) }) - it('should allow creation of new files in deeper paths', function (done) { + it('should not allow creation of new files in deeper paths', function (done) { var options = createOptions('/acl-tls/write-acl/empty-acl/test-folder/test-file', 'user1') options.body = '' request.put(options, function (error, response, body) { assert.equal(error, null) - assert.equal(response.statusCode, 201) + assert.equal(response.statusCode, 403) done() }) }) - it('Should create empty acl file', function (done) { + it('Should not create empty acl file', function (done) { var options = createOptions('/acl-tls/write-acl/empty-acl/another-empty-folder/test-file.acl', 'user1') options.headers = { 'content-type': 'text/turtle' @@ -202,19 +203,19 @@ describe('ACL with WebID+TLS', function () { options.body = '' request.put(options, function (error, response, body) { assert.equal(error, null) - assert.equal(response.statusCode, 201) + assert.equal(response.statusCode, 403) done() }) }) - it('should return text/turtle for the acl file', function (done) { + it('should not return text/turtle for the acl file', function (done) { var options = createOptions('/acl-tls/write-acl/.acl', 'user1') options.headers = { accept: 'text/turtle' } request.get(options, function (error, response, body) { assert.equal(error, null) - assert.equal(response.statusCode, 200) - assert.match(response.headers['content-type'], /text\/turtle/) + assert.equal(response.statusCode, 403) + // assert.match(response.headers['content-type'], /text\/turtle/) done() }) }) @@ -242,15 +243,15 @@ describe('ACL with WebID+TLS', function () { done() }) }) - it("should access test file's acl file", function (done) { + it("should not access test file's acl file", function (done) { var options = createOptions('/acl-tls/write-acl/test-file.acl', 'user1') options.headers = { accept: 'text/turtle' } request.get(options, function (error, response, body) { assert.equal(error, null) - assert.equal(response.statusCode, 200) - assert.match(response.headers['content-type'], /text\/turtle/) + assert.equal(response.statusCode, 403) + // assert.match(response.headers['content-type'], /text\/turtle/) done() }) }) @@ -271,18 +272,18 @@ describe('ACL with WebID+TLS', function () { }) it('should PUT new ACL file', function (done) { - var options = createOptions('/acl-tls/origin/test-folder/.acl', 'user1') + var options = createOptions('/acl-tls/origin/test-folder/.acl', 'user1', 'text/turtle') options.headers = { 'content-type': 'text/turtle' } options.body = '<#Owner> a ;\n' + - ' ;\n' + + ' ;\n' + ' <' + user1 + '>;\n' + ' <' + origin1 + '>;\n' + ' , , .\n' + '<#Public> a ;\n' + ' <./>;\n' + - ' ;\n' + + ' ;\n' + ' <' + origin1 + '>;\n' + ' .\n' request.put(options, function (error, response, body) { @@ -314,24 +315,24 @@ describe('ACL with WebID+TLS', function () { done() }) }) - it('user1 should be able to access test directory when origin is invalid', + it('user1 should not be able to access test directory when origin is invalid', function (done) { var options = createOptions('/acl-tls/origin/test-folder/', 'user1') options.headers.origin = origin2 request.head(options, function (error, response, body) { assert.equal(error, null) - assert.equal(response.statusCode, 200) + assert.equal(response.statusCode, 403) done() }) }) - it('agent should be able to access test directory', function (done) { + it('agent not should be able to access test directory', function (done) { var options = createOptions('/acl-tls/origin/test-folder/') options.headers.origin = origin1 request.head(options, function (error, response, body) { assert.equal(error, null) - assert.equal(response.statusCode, 200) + assert.equal(response.statusCode, 401) done() }) }) @@ -346,14 +347,14 @@ describe('ACL with WebID+TLS', function () { done() }) }) - it('agent should be able to access test directory when origin is invalid', + it('agent should not be able to access test directory when origin is invalid', function (done) { var options = createOptions('/acl-tls/origin/test-folder/') options.headers.origin = origin2 request.head(options, function (error, response, body) { assert.equal(error, null) - assert.equal(response.statusCode, 200) + assert.equal(response.statusCode, 401) done() }) }) @@ -369,21 +370,21 @@ describe('ACL with WebID+TLS', function () { }) it('should PUT new ACL file', function (done) { - var options = createOptions('/acl-tls/origin/test-folder/.acl', 'user1') + var options = createOptions('/acl-tls/origin/test-folder/.acl', 'user1', 'text/turtle') options.headers = { 'content-type': 'text/turtle' } options.body = '<#Owner1> a ;\n' + - ' ;\n' + + ' ;\n' + ' <' + user1 + '>;\n' + ' , , .\n' + '<#Owner2> a ;\n' + - ' ;\n' + + ' ;\n' + ' <' + origin1 + '>;\n' + ' , , .\n' + '<#Public> a ;\n' + ' <./>;\n' + - ' ;\n' + + ' ;\n' + ' <' + origin1 + '>;\n' + ' .\n' request.put(options, function (error, response, body) { @@ -415,24 +416,24 @@ describe('ACL with WebID+TLS', function () { done() }) }) - it('user1 should be able to access test directory when origin is invalid', + it('user1 should not be able to access test directory when origin is invalid', function (done) { var options = createOptions('/acl-tls/origin/test-folder/', 'user1') options.headers.origin = origin2 request.head(options, function (error, response, body) { assert.equal(error, null) - assert.equal(response.statusCode, 200) + assert.equal(response.statusCode, 403) done() }) }) - it('agent should be able to access test directory', function (done) { + it('agent should not be able to access test directory for logged in users', function (done) { var options = createOptions('/acl-tls/origin/test-folder/') options.headers.origin = origin1 request.head(options, function (error, response, body) { assert.equal(error, null) - assert.equal(response.statusCode, 200) + assert.equal(response.statusCode, 401) done() }) }) @@ -447,14 +448,14 @@ describe('ACL with WebID+TLS', function () { done() }) }) - it('agent should be able to access test directory when origin is invalid', + it('agent should not be able to access test directory when origin is invalid', function (done) { var options = createOptions('/acl-tls/origin/test-folder/') options.headers.origin = origin2 request.head(options, function (error, response, body) { assert.equal(error, null) - assert.equal(response.statusCode, 200) + assert.equal(response.statusCode, 401) done() }) }) @@ -589,7 +590,7 @@ describe('ACL with WebID+TLS', function () { done() }) }) - it('user1 should be able to PATCH a resource', function (done) { + it.skip('user1 should be able to PATCH a resource', function (done) { var options = createOptions('/acl-tls/append-inherited/test.ttl', 'user1') options.headers = { 'content-type': 'application/sparql-update' @@ -774,7 +775,7 @@ describe('ACL with WebID+TLS', function () { }) }) - describe('defaultForNew', function () { + describe('default', function () { before(function () { rm('/acl-tls/write-acl/default-for-new/.acl') rm('/acl-tls/write-acl/default-for-new/test-file.ttl') @@ -783,11 +784,11 @@ describe('ACL with WebID+TLS', function () { var body = '<#Owner> a ;\n' + ' <./>;\n' + ' <' + user1 + '>;\n' + - ' <./>;\n' + + ' <./>;\n' + ' , , .\n' + '<#Default> a ;\n' + ' <./>;\n' + - ' <./>;\n' + + ' <./>;\n' + ' ;\n' + ' .\n' it("user1 should be able to modify test directory's ACL file", function (done) { diff --git a/test/integration/auth-proxy-test.js b/test/integration/auth-proxy-test.js index f1062ce04..5bc601e5e 100644 --- a/test/integration/auth-proxy-test.js +++ b/test/integration/auth-proxy-test.js @@ -36,7 +36,8 @@ describe('Auth Proxy', () => { rm('index.html.acl') }) - describe('responding to /server/a', () => { + // Skipped tests due to not supported deep acl:accessTo #963 + describe.skip('responding to /server/a', () => { let response before(() => request(server).get('/server/a/') @@ -49,7 +50,7 @@ describe('Auth Proxy', () => { }) describe('responding to GET', () => { - describe('for a path with read permissions', () => { + describe.skip('for a path with read permissions', () => { let response before(() => request(server).get('/server/a/r') @@ -74,7 +75,7 @@ describe('Auth Proxy', () => { }) describe('responding to OPTIONS', () => { - describe('for a path with read permissions', () => { + describe.skip('for a path with read permissions', () => { let response before(() => request(server).options('/server/a/r') @@ -99,7 +100,7 @@ describe('Auth Proxy', () => { }) describe('responding to POST', () => { - describe('for a path with read and write permissions', () => { + describe.skip('for a path with read and write permissions', () => { let response before(() => request(server).post('/server/a/rw') diff --git a/test/integration/authentication-oidc-test.js b/test/integration/authentication-oidc-test.js index 21aeed093..e2153a946 100644 --- a/test/integration/authentication-oidc-test.js +++ b/test/integration/authentication-oidc-test.js @@ -10,7 +10,7 @@ const localStorage = require('localstorage-memory') const URL = require('whatwg-url').URL global.URL = URL global.URLSearchParams = require('whatwg-url').URLSearchParams -const { cleanDir } = require('../utils') +const { cleanDir, cp } = require('../utils') const supertest = require('supertest') const chai = require('chai') @@ -20,7 +20,7 @@ chai.use(require('dirty-chai')) // In this test we always assume that we are Alice describe('Authentication API (OIDC)', () => { - let alice, bob + let alice, bob // eslint-disable-line no-unused-vars let aliceServerUri = 'https://localhost:7000' let aliceWebId = 'https://localhost:7000/profile/card#me' @@ -35,6 +35,8 @@ describe('Authentication API (OIDC)', () => { let bobDbPath = path.join(__dirname, '../resources/accounts-scenario/bob/db') + const trustedAppUri = 'https://trusted.app' + const serverConfig = { sslKey: path.join(__dirname, '../keys/key.pem'), sslCert: path.join(__dirname, '../keys/cert.pem'), @@ -42,7 +44,8 @@ describe('Authentication API (OIDC)', () => { dataBrowser: false, webid: true, multiuser: false, - configPath + configPath, + trustedOrigins: ['https://apps.solid.invalid'] } const aliceRootPath = path.join(__dirname, '../resources/accounts-scenario/alice') @@ -68,14 +71,16 @@ describe('Authentication API (OIDC)', () => { }) } - before(() => { - return Promise.all([ + before(async () => { + await Promise.all([ startServer(alicePod, 7000), startServer(bobPod, 7001) ]).then(() => { alice = supertest(aliceServerUri) bob = supertest(bobServerUri) }) + cp(path.join('accounts-scenario/alice', '.acl-override'), path.join('accounts-scenario/alice', '.acl')) + cp(path.join('accounts-scenario/bob', '.acl-override'), path.join('accounts-scenario/bob', '.acl')) }) after(() => { @@ -132,7 +137,7 @@ describe('Authentication API (OIDC)', () => { }) it('should set the cookie', () => { - expect(cookie).to.match(/connect.sid=/) + expect(cookie).to.match(/connect.sid=\S{65,100}/) }) it('should set the cookie with HttpOnly', () => { @@ -147,7 +152,7 @@ describe('Authentication API (OIDC)', () => { describe('without that cookie', () => { let response before(done => { - alice.get('/') + alice.get('/private-for-alice.txt') .end((err, res) => { response = res done(err) @@ -162,7 +167,7 @@ describe('Authentication API (OIDC)', () => { describe('with that cookie and a non-matching origin', () => { let response before(done => { - alice.get('/') + alice.get('/private-for-owner.txt') .set('Cookie', cookie) .set('Origin', bobServerUri) .end((err, res) => { @@ -171,6 +176,39 @@ describe('Authentication API (OIDC)', () => { }) }) + it('should return a 403', () => { + expect(response).to.have.property('status', 403) + }) + }) + + describe('with that cookie and a non-matching origin', () => { + let response + before(done => { + alice.get('/private-for-alice.txt') + .set('Cookie', cookie) + .set('Origin', bobServerUri) + .end((err, res) => { + response = res + done(err) + }) + }) + + it('should return a 403', () => { + expect(response).to.have.property('status', 403) + }) + }) + + describe('without that cookie and a non-matching origin', () => { + let response + before(done => { + alice.get('/private-for-alice.txt') + .set('Origin', bobServerUri) + .end((err, res) => { + response = res + done(err) + }) + }) + it('should return a 401', () => { expect(response).to.have.property('status', 401) }) @@ -192,7 +230,39 @@ describe('Authentication API (OIDC)', () => { }) }) - describe('with that cookie and a matching origin', () => { + describe('with that cookie, private resource and no origin set', () => { + before(done => { + alice.get('/private-for-alice.txt') + .set('Cookie', cookie) + .end((err, res) => { + response = res + done(err) + }) + }) + + it('should return a 200', () => expect(response).to.have.property('status', 200)) + }) + + // How Mallory might set their cookie: + describe('with malicious cookie but without origin', () => { + let response + before(done => { + var malcookie = cookie.replace(/connect\.sid=(\S+)/, 'connect.sid=l33th4x0rzp0wn4g3;') + alice.get('/private-for-alice.txt') + .set('Cookie', malcookie) + .end((err, res) => { + response = res + done(err) + }) + }) + + it('should return a 401', () => { + expect(response).to.have.property('status', 401) + }) + }) + + // Our origin is trusted by default + describe('with that cookie and our origin', () => { let response before(done => { alice.get('/') @@ -208,6 +278,209 @@ describe('Authentication API (OIDC)', () => { expect(response).to.have.property('status', 200) }) }) + + // Another origin isn't trusted by default + describe('with that cookie and our origin', () => { + let response + before(done => { + alice.get('/private-for-owner.txt') + .set('Cookie', cookie) + .set('Origin', 'https://some.other.domain.com') + .end((err, res) => { + response = res + done(err) + }) + }) + + it('should return a 403', () => { + expect(response).to.have.property('status', 403) + }) + }) + + // Our own origin, no agent auth + describe('without that cookie but with our origin', () => { + let response + before(done => { + alice.get('/private-for-owner.txt') + .set('Origin', aliceServerUri) + .end((err, res) => { + response = res + done(err) + }) + }) + + it('should return a 401', () => { + expect(response).to.have.property('status', 401) + }) + }) + + // Configuration for originsAllowed + describe('with that cookie but with globally configured origin', () => { + let response + before(done => { + alice.get('/') + .set('Cookie', cookie) + .set('Origin', 'https://apps.solid.invalid') + .end((err, res) => { + response = res + done(err) + }) + }) + + it('should return a 200', () => { + expect(response).to.have.property('status', 200) + }) + }) + + // Configuration for originsAllowed but no auth + describe('without that cookie but with globally configured origin', () => { + let response + before(done => { + alice.get('/private-for-alice.txt') + .set('Origin', 'https://apps.solid.invalid') + .end((err, res) => { + response = res + done(err) + }) + }) + + it('should return a 401', () => { + expect(response).to.have.property('status', 401) + }) + }) + + // Configuration for originsAllowed with malicious cookie + describe('with malicious cookie but with globally configured origin', () => { + let response + before(done => { + var malcookie = cookie.replace(/connect\.sid=(\S+)/, 'connect.sid=l33th4x0rzp0wn4g3;') + alice.get('/private-for-alice.txt') + .set('Cookie', malcookie) + .set('Origin', 'https://apps.solid.invalid') + .end((err, res) => { + response = res + done(err) + }) + }) + + it('should return a 401', () => { + expect(response).to.have.property('status', 401) + }) + }) + + // Not authenticated but also wrong origin, + // 403 because authenticating wouldn't help, since the Origin is wrong + describe('without that cookie and a matching origin', () => { + let response + before(done => { + alice.get('/private-for-owner.txt') + .set('Origin', bobServerUri) + .end((err, res) => { + response = res + done(err) + }) + }) + + it('should return a 401', () => { + expect(response).to.have.property('status', 401) + }) + }) + + // Authenticated but origin not OK + describe('with that cookie and a non-matching origin', () => { + let response + before(done => { + alice.get('/private-for-owner.txt') + .set('Cookie', cookie) + .set('Origin', bobServerUri) + .end((err, res) => { + response = res + done(err) + }) + }) + + it('should return a 403', () => { + expect(response).to.have.property('status', 403) + }) + }) + + describe('with malicious cookie and our origin', () => { + let response + before(done => { + var malcookie = cookie.replace(/connect\.sid=(\S+)/, 'connect.sid=l33th4x0rzp0wn4g3;') + alice.get('/private-for-alice.txt') + .set('Cookie', malcookie) + .set('Origin', aliceServerUri) + .end((err, res) => { + response = res + done(err) + }) + }) + + it('should return a 401', () => { + expect(response).to.have.property('status', 401) + }) + }) + + describe('with malicious cookie and a non-matching origin', () => { + let response + before(done => { + var malcookie = cookie.replace(/connect\.sid=(\S+)/, 'connect.sid=l33th4x0rzp0wn4g3;') + alice.get('/private-for-owner.txt') + .set('Cookie', malcookie) + .set('Origin', bobServerUri) + .end((err, res) => { + response = res + done(err) + }) + }) + + it('should return a 401', () => { + expect(response).to.have.property('status', 401) + }) + }) + + describe('with trusted app and no cookie', () => { + before(done => { + alice.get('/private-for-alice.txt') + .set('Origin', trustedAppUri) + .end((err, res) => { + response = res + done(err) + }) + }) + + it('should return a 401', () => expect(response).to.have.property('status', 401)) + }) + + describe('with trusted app and malicious cookie', () => { + before(done => { + var malcookie = cookie.replace(/connect\.sid=(\S+)/, 'connect.sid=l33th4x0rzp0wn4g3;') + alice.get('/private-for-alice.txt') + .set('Cookie', malcookie) + .set('Origin', trustedAppUri) + .end((err, res) => { + response = res + done(err) + }) + }) + + it('should return a 401', () => expect(response).to.have.property('status', 401)) + }) + + describe('with trusted app and correct cookie', () => { + before(done => { + alice.get('/private-for-alice.txt') + .set('Cookie', cookie) + .set('Origin', trustedAppUri) + .end((err, res) => { + response = res + done(err) + }) + }) + + it('should return a 200', () => expect(response).to.have.property('status', 200)) + }) }) }) @@ -237,9 +510,9 @@ describe('Authentication API (OIDC)', () => { describe('Browser login workflow', () => { it('401 Unauthorized asking the user to log in', (done) => { bob.get('/shared-with-alice.txt') - .expect(401) - .end((err, res) => { - expect(res.text).to.contain('Log in') + .end((err, { status, text }) => { + expect(status).to.equal(401) + expect(text).to.contain('Log in') done(err) }) }) @@ -374,7 +647,7 @@ describe('Authentication API (OIDC)', () => { // Step 6: Web App extracts tokens from the uri hash fragment, uses // them to access protected resource - it('should use id token from the callback uri to access shared resource', () => { + it('should use id token from the callback uri to access shared resource (no origin)', () => { auth.window.location.href = callbackUri let protectedResourcePath = bobServerUri + '/shared-with-alice.txt' @@ -404,6 +677,32 @@ describe('Authentication API (OIDC)', () => { }) }) + it('should use id token from the callback uri to access shared resource (untrusted origin)', () => { + auth.window.location.href = callbackUri + + let protectedResourcePath = bobServerUri + '/shared-with-alice.txt' + + return auth.initUserFromResponse(auth.currentClient) + .then(webId => { + expect(webId).to.equal(aliceWebId) + + return auth.issuePoPTokenFor(bobServerUri, auth.session) + }) + .then(popToken => { + bearerToken = popToken + + return fetch(protectedResourcePath, { + headers: { + 'Authorization': 'Bearer ' + bearerToken, + 'Origin': 'https://untrusted.example.com' // shouldn't be allowed if strictOrigin is set to true + } + }) + }) + .then(res => { + expect(res.status).to.equal(403) + }) + }) + it('should not be able to reuse the bearer token for bob server on another server', () => { let privateAliceResourcePath = aliceServerUri + '/private-for-alice.txt' diff --git a/test/integration/authentication-oidc-with-strict-origins-turned-off-test.js b/test/integration/authentication-oidc-with-strict-origins-turned-off-test.js new file mode 100644 index 000000000..7cf97db3a --- /dev/null +++ b/test/integration/authentication-oidc-with-strict-origins-turned-off-test.js @@ -0,0 +1,605 @@ +const Solid = require('../../index') +const path = require('path') +const fs = require('fs-extra') +const { UserStore } = require('@solid/oidc-auth-manager') +const UserAccount = require('../../lib/models/user-account') +const SolidAuthOIDC = require('@solid/solid-auth-oidc') + +const fetch = require('node-fetch') +const localStorage = require('localstorage-memory') +const URL = require('whatwg-url').URL +global.URL = URL +global.URLSearchParams = require('whatwg-url').URLSearchParams +const { cleanDir, cp } = require('../utils') + +const supertest = require('supertest') +const chai = require('chai') +const expect = chai.expect +chai.use(require('dirty-chai')) + +// In this test we always assume that we are Alice + +describe('Authentication API (OIDC) - With strict origins turned off', () => { + let alice, bob + + const aliceServerPort = 7010 + const aliceServerUri = `https://localhost:${aliceServerPort}` + const aliceWebId = `https://localhost:${aliceServerPort}/profile/card#me` + let configPath = path.join(__dirname, '../resources/config') + let aliceDbPath = path.join(__dirname, '../resources/accounts-strict-origin-off/alice/db') + let userStorePath = path.join(aliceDbPath, 'oidc/users') + let aliceUserStore = UserStore.from({ path: userStorePath, saltRounds: 1 }) + aliceUserStore.initCollections() + + const bobServerPort = 7011 + const bobServerUri = `https://localhost:${bobServerPort}` + let bobDbPath = path.join(__dirname, '../resources/accounts-strict-origin-off/bob/db') + + const trustedAppUri = 'https://trusted.app' + + const serverConfig = { + sslKey: path.join(__dirname, '../keys/key.pem'), + sslCert: path.join(__dirname, '../keys/cert.pem'), + auth: 'oidc', + dataBrowser: false, + webid: true, + multiuser: false, + configPath, + strictOrigin: false + } + + const aliceRootPath = path.join(__dirname, '../resources/accounts-strict-origin-off/alice') + const alicePod = Solid.createServer( + Object.assign({ + root: aliceRootPath, + serverUri: aliceServerUri, + dbPath: aliceDbPath + }, serverConfig) + ) + const bobRootPath = path.join(__dirname, '../resources/accounts-strict-origin-off/bob') + const bobPod = Solid.createServer( + Object.assign({ + root: bobRootPath, + serverUri: bobServerUri, + dbPath: bobDbPath + }, serverConfig) + ) + + function startServer (pod, port) { + return new Promise((resolve) => { + pod.listen(port, () => { resolve() }) + }) + } + + before(async () => { + await Promise.all([ + startServer(alicePod, aliceServerPort), + startServer(bobPod, bobServerPort) + ]).then(() => { + alice = supertest(aliceServerUri) + bob = supertest(bobServerUri) + }) + cp(path.join('accounts-strict-origin-off/alice', '.acl-override'), path.join('accounts-strict-origin-off/alice', '.acl')) + cp(path.join('accounts-strict-origin-off/bob', '.acl-override'), path.join('accounts-strict-origin-off/bob', '.acl')) + }) + + after(() => { + alicePod.close() + bobPod.close() + fs.removeSync(path.join(aliceDbPath, 'oidc/users')) + cleanDir(aliceRootPath) + cleanDir(bobRootPath) + }) + + describe('Login page (GET /login)', () => { + it('should load the user login form', () => alice.get('/login').expect(200)) + }) + + describe('Login by Username and Password (POST /login/password)', () => { + // Logging in as alice, to alice's pod + let aliceAccount = UserAccount.from({ webId: aliceWebId }) + let alicePassword = '12345' + + beforeEach(() => { + aliceUserStore.initCollections() + + return aliceUserStore.createUser(aliceAccount, alicePassword) + .catch(console.error.bind(console)) + }) + + afterEach(() => { + fs.removeSync(path.join(aliceDbPath, 'users/users')) + }) + + describe('after performing a correct login', () => { + let response, cookie + before(done => { + aliceUserStore.initCollections() + aliceUserStore.createUser(aliceAccount, alicePassword) + alice.post('/login/password') + .type('form') + .send({ username: 'alice' }) + .send({ password: alicePassword }) + .end((err, res) => { + response = res + cookie = response.headers['set-cookie'][0] + done(err) + }) + }) + + it('should redirect to /authorize', () => { + const loginUri = response.headers.location + expect(response).to.have.property('status', 302) + expect(loginUri.startsWith(aliceServerUri + '/authorize')) + }) + + it('should set the cookie', () => { + expect(cookie).to.match(/connect.sid=\S{65,100}/) + }) + + it('should set the cookie with HttpOnly', () => { + expect(cookie).to.match(/HttpOnly/) + }) + + it('should set the cookie with Secure', () => { + expect(cookie).to.match(/Secure/) + }) + + describe('and performing a subsequent request', () => { + let response + describe('without cookie', () => { + describe('and no origin set', () => { + before(done => { + alice.get('/private-for-alice.txt') + .end((err, res) => { + response = res + done(err) + }) + }) + + it('should return a 401', () => expect(response).to.have.property('status', 401)) + }) + describe('and our origin', () => { + // Our own origin, no agent auth + before(done => { + alice.get('/private-for-alice.txt') + .set('Origin', aliceServerUri) + .end((err, res) => { + response = res + done(err) + }) + }) + + it('should return a 401', () => expect(response).to.have.property('status', 401)) + }) + describe('and trusted origin', () => { + // Configuration for originsAllowed but no auth + before(done => { + alice.get('/private-for-alice.txt') + .set('Origin', 'https://apps.solid.invalid') + .end((err, res) => { + response = res + done(err) + }) + }) + + it('should return a 401', () => expect(response).to.have.property('status', 401)) + }) + describe('and untrusted origin', () => { + // Not authenticated but also wrong origin, + before(done => { + alice.get('/private-for-alice.txt') + .set('Origin', bobServerUri) + .end((err, res) => { + response = res + done(err) + }) + }) + + it('should return a 401', () => expect(response).to.have.property('status', 401)) + }) + describe('and trusted app', () => { + before(done => { + alice.get('/private-for-alice.txt') + .set('Origin', trustedAppUri) + .end((err, res) => { + response = res + done(err) + }) + }) + + it('should return a 401', () => expect(response).to.have.property('status', 401)) + }) + }) + + describe('with cookie', () => { + describe('and no origin set', () => { + before(done => { + alice.get('/private-for-alice.txt') + .set('Cookie', cookie) + .end((err, res) => { + response = res + done(err) + }) + }) + + it('should return a 200', () => expect(response).to.have.property('status', 200)) + }) + describe('and our origin', () => { + before(done => { + alice.get('/private-for-alice.txt') + .set('Cookie', cookie) + .set('Origin', aliceServerUri) + .end((err, res) => { + response = res + done(err) + }) + }) + + it('should return a 200', () => expect(response).to.have.property('status', 200)) + }) + describe('and trusted origin', () => { + before(done => { + alice.get('/') + .set('Cookie', cookie) + .set('Origin', 'https://apps.solid.invalid') // TODO: Should we configure the server with that? Should it matter? + .end((err, res) => { + response = res + done(err) + }) + }) + + it('should return a 401', () => expect(response).to.have.property('status', 401)) + }) + describe('and untrusted origin', () => { + before(done => { + alice.get('/private-for-alice.txt') + .set('Cookie', cookie) + .set('Origin', bobServerUri) + .end((err, res) => { + response = res + done(err) + }) + }) + + // Even if origin checking is disabled, then this should return a 401 because cookies should not be trusted cross-origin + it('should return a 401', () => expect(response).to.have.property('status', 401)) + }) + + describe('and trusted app', () => { + // Trusted apps are not supported when strictOrigin check is turned off + before(done => { + alice.get('/private-for-alice.txt') + .set('Cookie', cookie) + .set('Origin', trustedAppUri) + .end((err, res) => { + response = res + done(err) + }) + }) + + it('should return a 401', () => expect(response).to.have.property('status', 401)) + }) + }) + + describe('with malicious cookie', () => { + let malcookie + before(() => { + // How Mallory might set their cookie: + malcookie = cookie.replace(/connect\.sid=(\S+)/, 'connect.sid=l33th4x0rzp0wn4g3;') + }) + describe('and no origin set', () => { + before(done => { + alice.get('/private-for-alice.txt') + .set('Cookie', malcookie) + .end((err, res) => { + response = res + done(err) + }) + }) + + it('should return a 401', () => expect(response).to.have.property('status', 401)) + }) + describe('and our origin', () => { + before(done => { + alice.get('/private-for-alice.txt') + .set('Cookie', malcookie) + .set('Origin', aliceServerUri) + .end((err, res) => { + response = res + done(err) + }) + }) + + it('should return a 401', () => expect(response).to.have.property('status', 401)) + }) + describe('and trusted origin', () => { + before(done => { + alice.get('/private-for-alice.txt') + .set('Cookie', malcookie) + .set('Origin', 'https://apps.solid.invalid') + .end((err, res) => { + response = res + done(err) + }) + }) + + it('should return a 401', () => expect(response).to.have.property('status', 401)) + }) + describe('and untrusted origin', () => { + before(done => { + alice.get('/private-for-alice.txt') + .set('Cookie', malcookie) + .set('Origin', bobServerUri) + .end((err, res) => { + response = res + done(err) + }) + }) + + it('should return a 401', () => expect(response).to.have.property('status', 401)) + }) + + describe('and trusted app', () => { + before(done => { + alice.get('/private-for-alice.txt') + .set('Cookie', malcookie) + .set('Origin', trustedAppUri) + .end((err, res) => { + response = res + done(err) + }) + }) + + it('should return a 401', () => expect(response).to.have.property('status', 401)) + }) + }) + }) + }) + + it('should throw a 400 if no username is provided', (done) => { + alice.post('/login/password') + .type('form') + .send({ password: alicePassword }) + .expect(400, done) + }) + + it('should throw a 400 if no password is provided', (done) => { + alice.post('/login/password') + .type('form') + .send({ username: 'alice' }) + .expect(400, done) + }) + + it('should throw a 400 if user is found but no password match', (done) => { + alice.post('/login/password') + .type('form') + .send({ username: 'alice' }) + .send({ password: 'wrongpassword' }) + .expect(400, done) + }) + }) + + describe('Browser login workflow', () => { + it('401 Unauthorized asking the user to log in', (done) => { + bob.get('/shared-with-alice.txt') + .end((err, { status, text }) => { + expect(status).to.equal(401) + expect(text).to.contain('Log in') + done(err) + }) + }) + }) + + describe('Two Pods + Web App Login Workflow', () => { + let aliceAccount = UserAccount.from({ webId: aliceWebId }) + let alicePassword = '12345' + + let auth + let authorizationUri, loginUri, authParams, callbackUri + let loginFormFields = '' + let bearerToken + + before(() => { + auth = new SolidAuthOIDC({ store: localStorage, window: { location: {} } }) + let appOptions = { + redirectUri: 'https://app.example.com/callback' + } + + aliceUserStore.initCollections() + + return aliceUserStore.createUser(aliceAccount, alicePassword) + .then(() => { + return auth.registerClient(aliceServerUri, appOptions) + }) + .then(registeredClient => { + auth.currentClient = registeredClient + }) + }) + + after(() => { + fs.removeSync(path.join(aliceDbPath, 'users/users')) + fs.removeSync(path.join(aliceDbPath, 'oidc/op/tokens')) + + let clientId = auth.currentClient.registration['client_id'] + let registration = `_key_${clientId}.json` + fs.removeSync(path.join(aliceDbPath, 'oidc/op/clients', registration)) + }) + + // Step 1: An app makes a GET request and receives a 401 + it('should get a 401 error on a REST request to a protected resource', () => { + return fetch(bobServerUri + '/shared-with-alice.txt') + .then(res => { + expect(res.status).to.equal(401) + + expect(res.headers.get('www-authenticate')) + .to.equal(`Bearer realm="${bobServerUri}", scope="openid webid"`) + }) + }) + + // Step 2: App presents the Select Provider UI to user, determine the + // preferred provider uri (here, aliceServerUri), and constructs + // an authorization uri for that provider + it('should determine the authorization uri for a preferred provider', () => { + return auth.currentClient.createRequest({}, auth.store) + .then(authUri => { + authorizationUri = authUri + + expect(authUri.startsWith(aliceServerUri + '/authorize')).to.be.true() + }) + }) + + // Step 3: App redirects user to the authorization uri for login + it('should redirect user to /authorize and /login', () => { + return fetch(authorizationUri, { redirect: 'manual' }) + .then(res => { + // Since user is not logged in, /authorize redirects to /login + expect(res.status).to.equal(302) + + loginUri = new URL(res.headers.get('location')) + expect(loginUri.toString().startsWith(aliceServerUri + '/login')) + .to.be.true() + + authParams = loginUri.searchParams + }) + }) + + // Step 4: Pod returns a /login page with appropriate hidden form fields + it('should display the /login form', () => { + return fetch(loginUri.toString()) + .then(loginPage => { + return loginPage.text() + }) + .then(pageText => { + // Login page should contain the relevant auth params as hidden fields + + authParams.forEach((value, key) => { + let hiddenField = `` + + let fieldRegex = new RegExp(hiddenField) + + expect(pageText).to.match(fieldRegex) + + loginFormFields += `${key}=` + encodeURIComponent(value) + '&' + }) + }) + }) + + // Step 5: User submits their username & password via the /login form + it('should login via the /login form', () => { + loginFormFields += `username=${'alice'}&password=${alicePassword}` + + return fetch(aliceServerUri + '/login/password', { + method: 'POST', + body: loginFormFields, + redirect: 'manual', + headers: { + 'content-type': 'application/x-www-form-urlencoded' + }, + credentials: 'include' + }) + .then(res => { + expect(res.status).to.equal(302) + let postLoginUri = res.headers.get('location') + let cookie = res.headers.get('set-cookie') + + // Successful login gets redirected back to /authorize and then + // back to app + expect(postLoginUri.startsWith(aliceServerUri + '/authorize')) + .to.be.true() + + return fetch(postLoginUri, { redirect: 'manual', headers: { cookie } }) + }) + .then(res => { + // User gets redirected back to original app + expect(res.status).to.equal(302) + callbackUri = res.headers.get('location') + expect(callbackUri.startsWith('https://app.example.com#')) + }) + }) + + // Step 6: Web App extracts tokens from the uri hash fragment, uses + // them to access protected resource + it('should use id token from the callback uri to access shared resource (no origin)', () => { + auth.window.location.href = callbackUri + + let protectedResourcePath = bobServerUri + '/shared-with-alice.txt' + + return auth.initUserFromResponse(auth.currentClient) + .then(webId => { + expect(webId).to.equal(aliceWebId) + + return auth.issuePoPTokenFor(bobServerUri, auth.session) + }) + .then(popToken => { + bearerToken = popToken + + return fetch(protectedResourcePath, { + headers: { + 'Authorization': 'Bearer ' + bearerToken + } + }) + }) + .then(res => { + expect(res.status).to.equal(200) + + return res.text() + }) + .then(contents => { + expect(contents).to.equal('protected contents\n') + }) + }) + it('should use id token from the callback uri to access shared resource (untrusted origin)', () => { + auth.window.location.href = callbackUri + + let protectedResourcePath = bobServerUri + '/shared-with-alice.txt' + + return auth.initUserFromResponse(auth.currentClient) + .then(webId => { + expect(webId).to.equal(aliceWebId) + + return auth.issuePoPTokenFor(bobServerUri, auth.session) + }) + .then(popToken => { + bearerToken = popToken + + return fetch(protectedResourcePath, { + headers: { + 'Authorization': 'Bearer ' + bearerToken, + 'Origin': 'https://untrusted.example.com' // shouldn't matter if strictOrigin is set to false + } + }) + }) + .then(res => { + expect(res.status).to.equal(200) + + return res.text() + }) + .then(contents => { + expect(contents).to.equal('protected contents\n') + }) + }) + + it('should not be able to reuse the bearer token for bob server on another server', () => { + let privateAliceResourcePath = aliceServerUri + '/private-for-alice.txt' + + return fetch(privateAliceResourcePath, { + headers: { + // This is Alice's bearer token with her own Web ID + 'Authorization': 'Bearer ' + bearerToken + } + }) + .then(res => { + // It will get rejected; it was issued for Bob's server only + expect(res.status).to.equal(403) + }) + }) + }) + + describe('Post-logout page (GET /goodbye)', () => { + it('should load the post-logout page', () => { + return alice.get('/goodbye') + .expect(200) + }) + }) +}) diff --git a/test/integration/cors-proxy-test.js b/test/integration/cors-proxy-test.js index 9bcc9ee48..c4f643e55 100644 --- a/test/integration/cors-proxy-test.js +++ b/test/integration/cors-proxy-test.js @@ -1,18 +1,14 @@ var assert = require('chai').assert -var supertest = require('supertest') var path = require('path') var nock = require('nock') -var { checkDnsSettings } = require('../utils') - -var ldnode = require('../../index') +var { checkDnsSettings, setupSupertestServer } = require('../utils') describe('CORS Proxy', () => { - var ldp = ldnode({ + var server = setupSupertestServer({ root: path.join(__dirname, '../resources'), corsProxy: '/proxy', webid: false }) - var server = supertest(ldp) before(checkDnsSettings) diff --git a/test/integration/errors-oidc-test.js b/test/integration/errors-oidc-test.js index a3202fca5..eeafcd0ae 100644 --- a/test/integration/errors-oidc-test.js +++ b/test/integration/errors-oidc-test.js @@ -1,7 +1,7 @@ const supertest = require('supertest') const ldnode = require('../../index') const path = require('path') -const { cleanDir } = require('../utils') +const { cleanDir, cp } = require('../utils') const expect = require('chai').expect describe('OIDC error handling', function () { @@ -25,7 +25,10 @@ describe('OIDC error handling', function () { }) before(function (done) { - ldpHttpsServer = ldp.listen(3457, done) + ldpHttpsServer = ldp.listen(3457, () => { + cp(path.join('accounts/errortests', '.acl-override'), path.join('accounts/errortests', '.acl')) + done() + }) }) after(function () { diff --git a/test/integration/errors-test.js b/test/integration/errors-test.js index 8b73d5e2f..effe9f44d 100644 --- a/test/integration/errors-test.js +++ b/test/integration/errors-test.js @@ -1,30 +1,20 @@ -var supertest = require('supertest') var path = require('path') - -// Helper functions for the FS -// var rm = require('./utils').rm -// var write = require('./utils').write -// var cp = require('./utils').cp -var read = require('./../utils').read - -var ldnode = require('../../index') +const { read, setupSupertestServer } = require('./../utils') describe('Error pages', function () { // LDP with error pages - var errorLdp = ldnode({ + const errorServer = setupSupertestServer({ root: path.join(__dirname, '../resources'), errorPages: path.join(__dirname, '../resources/errorPages'), webid: false }) - var errorServer = supertest(errorLdp) // LDP with no error pages - var noErrorLdp = ldnode({ + const noErrorServer = setupSupertestServer({ root: path.join(__dirname, '../resources'), noErrorPages: true, webid: false }) - var noErrorServer = supertest(noErrorLdp) function defaultErrorPage (filepath, expected) { var handler = function (res) { diff --git a/test/integration/formats-test.js b/test/integration/formats-test.js index c82fcb4d1..9a6c04790 100644 --- a/test/integration/formats-test.js +++ b/test/integration/formats-test.js @@ -1,15 +1,13 @@ -var supertest = require('supertest') -var ldnode = require('../../index') var path = require('path') const assert = require('chai').assert +const { setupSupertestServer } = require('../utils') describe('formats', function () { - var ldp = ldnode.createServer({ + const server = setupSupertestServer({ root: path.join(__dirname, '../resources'), webid: false }) - var server = supertest(ldp) describe('HTML', function () { it('should return HTML containing "Hello, World!" if Accept is set to text/html', function (done) { server.get('/hello.html') @@ -95,8 +93,15 @@ describe('formats', function () { .expect(200, done) }) - it('should return turtle when listing container', function (done) { + it('should return turtle when listing container with an index page', function (done) { server.get('/sampleContainer/') + .set('accept', 'application/rdf+xml;q=0.4, application/xhtml+xml;q=0.3, text/xml;q=0.2, application/xml;q=0.2, text/html;q=0.3, text/plain;q=0.1, text/turtle;q=1.0, application/n3;q=1') + .expect('content-type', /text\/html/) + .expect(200, done) + }) + + it('should return turtle when listing container without an index page', function (done) { + server.get('/sampleContainer2/') .set('accept', 'application/rdf+xml;q=0.4, application/xhtml+xml;q=0.3, text/xml;q=0.2, application/xml;q=0.2, text/html;q=0.3, text/plain;q=0.1, text/turtle;q=1.0, application/n3;q=1') .expect('content-type', /text\/turtle/) .expect(200, done) diff --git a/test/integration/http-copy-test.js b/test/integration/http-copy-test.js index 56d660a38..a90a03852 100644 --- a/test/integration/http-copy-test.js +++ b/test/integration/http-copy-test.js @@ -8,7 +8,7 @@ var rm = require('./../utils').rm var solidServer = require('../../index') describe('HTTP COPY API', function () { - var address = 'https://localhost:3456' + var address = 'https://localhost:8443' var ldpHttpsServer var ldp = solidServer.createServer({ @@ -19,7 +19,7 @@ describe('HTTP COPY API', function () { }) before(function (done) { - ldpHttpsServer = ldp.listen(3456, done) + ldpHttpsServer = ldp.listen(8443, done) }) after(function () { diff --git a/test/integration/http-test.js b/test/integration/http-test.js index 1a650ec66..612133fa3 100644 --- a/test/integration/http-test.js +++ b/test/integration/http-test.js @@ -1,21 +1,19 @@ -var supertest = require('supertest') var fs = require('fs') var li = require('li') -var ldnode = require('../../index') var rm = require('./../utils').rm var path = require('path') const rdf = require('rdflib') +const { setupSupertestServer } = require('../utils') var suffixAcl = '.acl' var suffixMeta = '.meta' -var ldpServer = ldnode.createServer({ +var server = setupSupertestServer({ live: true, dataBrowserPath: 'default', root: path.join(__dirname, '../resources'), auth: 'oidc', webid: false }) -var server = supertest(ldpServer) var { assert, expect } = require('chai') /** @@ -58,16 +56,18 @@ function createTestResource (resourceName) { describe('HTTP APIs', function () { var emptyResponse = function (res) { if (res.text) { - console.log('Not empty response') + throw new Error('Not empty response') } } var getLink = function (res, rel) { - var links = res.headers.link.split(',') - for (var i in links) { - var link = links[i] - var parsedLink = li.parse(link) - if (parsedLink[rel]) { - return parsedLink[rel] + if (res.headers.link) { + var links = res.headers.link.split(',') + for (var i in links) { + var link = links[i] + var parsedLink = li.parse(link) + if (parsedLink[rel]) { + return parsedLink[rel] + } } } return undefined @@ -77,10 +77,10 @@ describe('HTTP APIs', function () { var link = getLink(res, rel) if (link) { if (link !== value) { - console.log('Not same value:', value, '!=', link) + throw new Error('Not same value: ' + value + ' != ' + link) } } else { - console.log(rel, 'header does not exist:', link) + throw new Error('header does not exist: ' + rel + ' = ' + value) } } return handler @@ -137,13 +137,13 @@ describe('HTTP APIs', function () { }) it('should return 204 on success', function (done) { - server.options('/sampleContainer/example1.ttl') + server.options('/sampleContainer2/example1.ttl') .expect(204) .end(done) }) it('should have Access-Control-Allow-Origin', function (done) { - server.options('/sampleContainer/example1.ttl') + server.options('/sampleContainer2/example1.ttl') .set('Origin', 'http://example.com') .expect('Access-Control-Allow-Origin', 'http://example.com') .end(done) @@ -151,20 +151,26 @@ describe('HTTP APIs', function () { it('should have set acl and describedBy Links for resource', function (done) { - server.options('/sampleContainer/example1.ttl') + server.options('/sampleContainer2/example1.ttl') .expect(hasHeader('acl', 'example1.ttl' + suffixAcl)) .expect(hasHeader('describedBy', 'example1.ttl' + suffixMeta)) .end(done) }) it('should have set Link as resource', function (done) { - server.options('/sampleContainer/example1.ttl') + server.options('/sampleContainer2/example1.ttl') .expect('Link', /; rel="type"/) .end(done) }) - it('should have set Link as Container/BasicContainer', function (done) { + it('should have set Link as resource on a implicit index page', function (done) { server.options('/sampleContainer/') + .expect('Link', /; rel="type"/) + .end(done) + }) + + it('should have set Link as Container/BasicContainer', function (done) { + server.options('/sampleContainer2/') .set('Origin', 'http://example.com') .expect('Link', /; rel="type"/) .expect('Link', /; rel="type"/) @@ -172,14 +178,14 @@ describe('HTTP APIs', function () { }) it('should have set Accept-Post for containers', function (done) { - server.options('/sampleContainer/') + server.options('/sampleContainer2/') .set('Origin', 'http://example.com') .expect('Accept-Post', '*/*') .end(done) }) it('should have set acl and describedBy Links for container', function (done) { - server.options('/sampleContainer/') + server.options('/sampleContainer2/') .expect(hasHeader('acl', suffixAcl)) .expect(hasHeader('describedBy', suffixMeta)) .end(done) @@ -205,7 +211,7 @@ describe('HTTP APIs', function () { }) it('should have Access-Control-Allow-Origin as Origin on containers', function (done) { - server.get('/sampleContainer/') + server.get('/sampleContainer2/') .set('Origin', 'http://example.com') .expect('content-type', /text\/turtle/) .expect('Access-Control-Allow-Origin', 'http://example.com') @@ -213,40 +219,40 @@ describe('HTTP APIs', function () { }) it('should have Access-Control-Allow-Origin as Origin on resources', function (done) { - server.get('/sampleContainer/example1.ttl') + server.get('/sampleContainer2/example1.ttl') .set('Origin', 'http://example.com') .expect('content-type', /text\/turtle/) .expect('Access-Control-Allow-Origin', 'http://example.com') .expect(200, done) }) it('should have set Link as resource', function (done) { - server.get('/sampleContainer/example1.ttl') + server.get('/sampleContainer2/example1.ttl') .expect('content-type', /text\/turtle/) .expect('Link', /; rel="type"/) .expect(200, done) }) it('should have set Updates-Via to use WebSockets', function (done) { - server.get('/sampleContainer/example1.ttl') + server.get('/sampleContainer2/example1.ttl') .expect('updates-via', /wss?:\/\//) .expect(200, done) }) it('should have set acl and describedBy Links for resource', function (done) { - server.get('/sampleContainer/example1.ttl') + server.get('/sampleContainer2/example1.ttl') .expect('content-type', /text\/turtle/) .expect(hasHeader('acl', 'example1.ttl' + suffixAcl)) .expect(hasHeader('describedBy', 'example1.ttl' + suffixMeta)) .end(done) }) it('should have set Link as Container/BasicContainer', function (done) { - server.get('/sampleContainer/') + server.get('/sampleContainer2/') .expect('content-type', /text\/turtle/) .expect('Link', /; rel="type"/) .expect('Link', /; rel="type"/) .expect(200, done) }) it('should load skin (mashlib) if resource was requested as text/html', function (done) { - server.get('/sampleContainer/example1.ttl') + server.get('/sampleContainer2/example1.ttl') .set('Accept', 'text/html') .expect('content-type', /text\/html/) .expect(function (res) { @@ -276,6 +282,19 @@ describe('HTTP APIs', function () { .end(done) }) + it('should NOT load data browser (mashlib) if directory has an index file', function (done) { + server.get('/sampleContainer/') + .set('Accept', 'text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8') + .expect('content-type', /text\/html/) + .expect(200) + .expect((res) => { + if (res.text.includes('TabulatorOutline')) { + throw new Error('Loaded data browser though resource has an .html extension') + } + }) + .end(done) + }) + it('should show data browser if container was requested as text/html', function (done) { server.get('/sampleContainer2/') .set('Accept', 'text/html') @@ -332,7 +351,7 @@ describe('HTTP APIs', function () { }) it('should have set acl and describedBy Links for container', function (done) { - server.get('/sampleContainer/') + server.get('/sampleContainer2/') .expect(hasHeader('acl', suffixAcl)) .expect(hasHeader('describedBy', suffixMeta)) .expect('content-type', /text\/turtle/) @@ -368,14 +387,20 @@ describe('HTTP APIs', function () { }) describe('HEAD API', function () { - it('should return content-type turtle by default', function (done) { + it('should return content-type application/octet-stream by default', function (done) { server.head('/sampleContainer/blank') - .expect('Content-Type', 'text/turtle; charset=utf-8') + .expect('Content-Type', 'application/octet-stream; charset=utf-8') .end(done) }) it('should have set content-type for turtle files', function (done) { - server.head('/sampleContainer/example1.ttl') + server.head('/sampleContainer2/example1.ttl') + .expect('Content-Type', 'text/turtle; charset=utf-8') + .end(done) + }) + it('should have set content-type for implicit turtle files', + function (done) { + server.head('/sampleContainer/example4') .expect('Content-Type', 'text/turtle; charset=utf-8') .end(done) }) @@ -386,7 +411,7 @@ describe('HTTP APIs', function () { .end(done) }) it('should have Access-Control-Allow-Origin as Origin', function (done) { - server.head('/sampleContainer/example1.ttl') + server.head('/sampleContainer2/example1.ttl') .set('Origin', 'http://example.com') .expect('Access-Control-Allow-Origin', 'http://example.com') .expect(200, done) @@ -397,32 +422,32 @@ describe('HTTP APIs', function () { .expect(200, done) }) it('should have set Updates-Via to use WebSockets', function (done) { - server.get('/sampleContainer/example1.ttl') + server.get('/sampleContainer2/example1.ttl') .expect('updates-via', /wss?:\/\//) .expect(200, done) }) it('should have set Link as Resource', function (done) { - server.head('/sampleContainer/example1.ttl') + server.head('/sampleContainer2/example1.ttl') .expect('Link', /; rel="type"/) .expect(200, done) }) it('should have set acl and describedBy Links for resource', function (done) { - server.get('/sampleContainer/example1.ttl') + server.get('/sampleContainer2/example1.ttl') .expect(hasHeader('acl', 'example1.ttl' + suffixAcl)) .expect(hasHeader('describedBy', 'example1.ttl' + suffixMeta)) .end(done) }) it('should have set Link as Container/BasicContainer', function (done) { - server.get('/sampleContainer/') + server.get('/sampleContainer2/') .expect('Link', /; rel="type"/) .expect('Link', /; rel="type"/) .expect(200, done) }) it('should have set acl and describedBy Links for container', function (done) { - server.get('/sampleContainer/') + server.get('/sampleContainer2/') .expect(hasHeader('acl', suffixAcl)) .expect(hasHeader('describedBy', suffixMeta)) .end(done) diff --git a/test/integration/ldp-test.js b/test/integration/ldp-test.js index 02769da55..27f5f2d04 100644 --- a/test/integration/ldp-test.js +++ b/test/integration/ldp-test.js @@ -1,10 +1,13 @@ -var assert = require('chai').assert +var chai = require('chai') +var assert = chai.assert +chai.use(require('chai-as-promised')) var $rdf = require('rdflib') var ns = require('solid-namespace')($rdf) var LDP = require('../../lib/ldp') var path = require('path') var stringToStream = require('../../lib/utils').stringToStream var randomBytes = require('randombytes') +var ResourceMapper = require('../../lib/resource-mapper') // Helper functions for the FS var rm = require('./../utils').rm @@ -14,66 +17,66 @@ var read = require('./../utils').read var fs = require('fs') describe('LDP', function () { + var root = path.join(__dirname, '..') + + var resourceMapper = new ResourceMapper({ + rootUrl: 'https://localhost:8443/', + rootPath: root, + includeHost: false + }) + var ldp = new LDP({ - root: path.join(__dirname, '..'), + resourceMapper, serverUri: 'https://localhost', multiuser: true, webid: false }) - describe('readFile', function () { - it('return 404 if file does not exist', function (done) { - ldp.readFile('resources/unexistent.ttl', function (err) { + describe('readResource', function () { + it('return 404 if file does not exist', () => { + return ldp.readResource('/resources/unexistent.ttl').catch(err => { assert.equal(err.status, 404) - done() }) }) - it('return file if file exists', function (done) { + it('return file if file exists', () => { // file can be empty as well write('hello world', 'fileExists.txt') - ldp.readFile(path.join(__dirname, '../resources/fileExists.txt'), function (err, file) { + return ldp.readResource('/resources/fileExists.txt').then(file => { rm('fileExists.txt') - assert.notOk(err) assert.equal(file, 'hello world') - done() }) }) }) - describe('readContainerMeta', function () { - it('should return 404 if .meta is not found', function (done) { - ldp.readContainerMeta('resources/', function (err) { + describe('readContainerMeta', () => { + it('should return 404 if .meta is not found', () => { + return ldp.readContainerMeta('/resources/').catch(err => { assert.equal(err.status, 404) - done() }) }) - it('should return content if metaFile exists', function (done) { + it('should return content if metaFile exists', () => { // file can be empty as well write('This function just reads this, does not parse it', '.meta') - ldp.readContainerMeta(path.join(__dirname, '../resources/'), function (err, metaFile) { + return ldp.readContainerMeta('/resources/').then(metaFile => { rm('.meta') - assert.notOk(err) assert.equal(metaFile, 'This function just reads this, does not parse it') - done() }) }) - it('should work also if trailing `/` is not passed', function (done) { + it('should work also if trailing `/` is not passed', () => { // file can be empty as well write('This function just reads this, does not parse it', '.meta') - ldp.readContainerMeta(path.join(__dirname, '../resources'), function (err, metaFile) { + return ldp.readContainerMeta('/resources').then(metaFile => { rm('.meta') - assert.notOk(err) assert.equal(metaFile, 'This function just reads this, does not parse it') - done() }) }) }) describe('getGraph', () => { - it.skip('should read and parse an existing file', () => { + it('should read and parse an existing file', () => { let uri = 'https://localhost:8443/resources/sampleContainer/example1.ttl' return ldp.getGraph(uri) .then(graph => { @@ -118,61 +121,117 @@ describe('LDP', function () { }) describe('put', function () { - it.skip('should write a file in an existing dir', function (done) { + it.skip('should write a file in an existing dir', () => { var stream = stringToStream('hello world') - ldp.put('localhost', '/resources/testPut.txt', stream, function (err) { - assert.notOk(err) + return ldp.put('/resources/testPut.txt', stream, 'text/plain').then(() => { var found = read('testPut.txt') rm('testPut.txt') assert.equal(found, 'hello world') - done() }) }) - it('should fail if a trailing `/` is passed', function (done) { + it('should fail if a trailing `/` is passed', () => { var stream = stringToStream('hello world') - ldp.put('localhost', '/resources/', stream, function (err) { + return ldp.put('/resources/', stream, 'text/plain').catch(err => { assert.equal(err.status, 409) - done() }) }) - it.skip('with a larger file to exceed allowed quota', function (done) { + it.skip('with a larger file to exceed allowed quota', function () { var randstream = stringToStream(randomBytes(2100)) - ldp.put('localhost', '/resources/testQuota.txt', randstream, function (err) { - console.log(err) + return ldp.put('localhost', '/resources/testQuota.txt', randstream).catch((err) => { assert.notOk(err) - done() }) }) - it.skip('should fail if a over quota', function (done) { + it('should fail if a over quota', function () { var hellostream = stringToStream('hello world') - ldp.put('localhost', '/resources/testOverQuota.txt', hellostream, function (err) { - console.log(err) + return ldp.put('localhost', '/resources/testOverQuota.txt', hellostream).catch((err) => { assert.equal(err.status, 413) - done() + }) + }) + + it('should fail if a trailing `/` is passed without content type', () => { + var stream = stringToStream('hello world') + return ldp.put('/resources/', stream, null).catch(err => { + assert.equal(err.status, 409) + }) + }) + + it('should fail if no content type is passed', () => { + var stream = stringToStream('hello world') + return ldp.put('/resources/testPut.txt', stream, null).catch(err => { + assert.equal(err.status, 415) }) }) }) describe('delete', function () { - it.skip('should delete a file in an existing dir', function (done) { + it('should error when deleting a non-existing file', () => { + return assert.isRejected(ldp.delete('/resources/testPut.txt')) + }) + + it.skip('should delete a file in an existing dir', async () => { + // First create a dummy file var stream = stringToStream('hello world') - ldp.put('localhost', '/resources/testPut.txt', stream, function (err) { - assert.notOk(err) - fs.stat(ldp.root + '/resources/testPut.txt', function (err) { - if (err) { - return done(err) - } - ldp.delete('localhost', '/resources/testPut.txt', function (err) { - if (err) done(err) - fs.stat(ldp.root + '/resources/testPut.txt', function (err) { - return done(err ? null : new Error('file still exists')) - }) - }) - }) + await ldp.put('/resources/testPut.txt', stream, 'text/plain') + // Make sure it exists + fs.stat(ldp.resourceMapper._rootPath + '/resources/testPut.txt', function (err) { + if (err) { + throw err + } + }) + + // Now delete the dummy file + await ldp.delete('/resources/testPut.txt') + // Make sure it does not exist anymore + fs.stat(ldp.resourceMapper._rootPath + '/resources/testPut.txt', function (err, s) { + if (!err) { + throw new Error('file still exists') + } }) }) + + it.skip('should fail to delete a non-empty folder', async () => { + // First create a dummy file + var stream = stringToStream('hello world') + await ldp.put('/resources/dummy/testPutBlocking.txt', stream, 'text/plain') + // Make sure it exists + fs.stat(ldp.resourceMapper._rootPath + '/resources/dummy/testPutBlocking.txt', function (err) { + if (err) { + throw err + } + }) + + // Now try to delete its folder + return assert.isRejected(ldp.delete('/resources/dummy/')) + }) + + it.skip('should fail to delete nested non-empty folders', async () => { + // First create a dummy file + var stream = stringToStream('hello world') + await ldp.put('/resources/dummy/dummy2/testPutBlocking.txt', stream, 'text/plain') + // Make sure it exists + fs.stat(ldp.resourceMapper._rootPath + '/resources/dummy/dummy2/testPutBlocking.txt', function (err) { + if (err) { + throw err + } + }) + + // Now try to delete its parent folder + return assert.isRejected(ldp.delete('/resources/dummy/')) + }) + + after(async function () { + // Clean up after delete tests + try { + await ldp.delete('/resources/dummy/testPutBlocking.txt') + await ldp.delete('/resources/dummy/dummy2/testPutBlocking.txt') + await ldp.delete('/resources/dummy/dummy2/') + await ldp.delete('/resources/dummy/') + } catch (err) { + + } + }) }) describe('listContainer', function () { /* @@ -183,7 +242,7 @@ describe('LDP', function () { ' dcterms:title "This is a magic type" ;' + ' o:limit 500000.00 .', 'sampleContainer/magicType.ttl') - ldp.listContainer(path.join(__dirname, '../resources/sampleContainer/'), 'https://server.tld/resources/sampleContainer/', 'https://server.tld', '', 'text/turtle', function (err, data) { + ldp.listContainer(path.join(__dirname, '../resources/sampleContainer/'), 'https://server.tld/resources/sampleContainer/', 'https://server.tld', '', 'application/octet-stream', function (err, data) { if (err) done(err) var graph = $rdf.graph() $rdf.parse( @@ -213,7 +272,7 @@ describe('LDP', function () { }) }) */ - it('should not inherit type of BasicContainer/Container if type is File', function (done) { + it('should not inherit type of BasicContainer/Container if type is File', () => { write('@prefix dcterms: .' + '@prefix o: .' + '<> a ;' + @@ -226,62 +285,67 @@ describe('LDP', function () { ' dcterms:title "This is a container" ;' + ' o:limit 500000.00 .', 'sampleContainer/basicContainerFile.ttl') - ldp.listContainer(path.join(__dirname, '../resources/sampleContainer/'), 'https://server.tld/resources/sampleContainer/', 'https://server.tld', '', 'text/turtle', function (err, data) { - if (err) done(err) - var graph = $rdf.graph() - $rdf.parse( - data, - graph, - 'https://server.tld/sampleContainer', - 'text/turtle') + return ldp.listContainer(path.join(__dirname, '../resources/sampleContainer/'), 'https://server.tld/resources/sampleContainer/', '', 'server.tld') + .then(data => { + var graph = $rdf.graph() + $rdf.parse( + data, + graph, + 'https://localhost:8443/resources/sampleContainer', + 'text/turtle') - var basicContainerStatements = graph - .each( - $rdf.sym('https://server.tld/basicContainerFile.ttl'), - ns.rdf('type'), - undefined - ) - .map(d => { return d.uri }) + var basicContainerStatements = graph + .each( + $rdf.sym('https://localhost:8443/resources/sampleContainer/basicContainerFile.ttl'), + ns.rdf('type'), + undefined + ) + .map(d => { return d.uri }) - let expectedStatements = [ - 'http://www.w3.org/ns/iana/media-types/text/turtle#Resource', - 'http://www.w3.org/ns/ldp#Resource' - ] - assert.deepEqual(basicContainerStatements.sort(), expectedStatements) + let expectedStatements = [ + 'http://www.w3.org/ns/iana/media-types/text/turtle#Resource', + 'http://www.w3.org/ns/ldp#Resource' + ] + assert.deepEqual(basicContainerStatements.sort(), expectedStatements) - var containerStatements = graph - .each( - $rdf.sym('https://server.tld/containerFile.ttl'), - ns.rdf('type'), - undefined - ) - .map(d => { return d.uri }) + var containerStatements = graph + .each( + $rdf.sym('https://localhost:8443/resources/sampleContainer/containerFile.ttl'), + ns.rdf('type'), + undefined + ) + .map(d => { return d.uri }) - assert.deepEqual(containerStatements.sort(), expectedStatements) + assert.deepEqual(containerStatements.sort(), expectedStatements) - rm('sampleContainer/containerFile.ttl') - rm('sampleContainer/basicContainerFile.ttl') - done() - }) + rm('sampleContainer/containerFile.ttl') + rm('sampleContainer/basicContainerFile.ttl') + }) }) - it('should ldp:contains the same files in dir', function (done) { - ldp.listContainer(path.join(__dirname, '../resources/sampleContainer/'), 'https://server.tld/resources/sampleContainer/', 'https://server.tld', '', 'text/turtle', function (err, data) { - if (err) done(err) - fs.readdir(path.join(__dirname, '../resources/sampleContainer/'), function (err, expectedFiles) { - const graph = $rdf.graph() - $rdf.parse(data, graph, 'https://server.tld/sampleContainer/', 'text/turtle') - const statements = graph.match(null, ns.ldp('contains'), null) - const files = statements - .map(s => s.object.value.replace(/.*\//, '')) - .map(decodeURIComponent) - - files.sort() - expectedFiles.sort() - assert.deepEqual(files, expectedFiles) - done(err) + it('should ldp:contains the same files in dir', () => { + ldp.listContainer(path.join(__dirname, '../resources/sampleContainer/'), 'https://server.tld/resources/sampleContainer/', '', 'server.tld') + .then(data => { + fs.readdir(path.join(__dirname, '../resources/sampleContainer/'), function (err, expectedFiles) { + // Strip dollar extension + expectedFiles = expectedFiles.map(ldp.resourceMapper._removeDollarExtension) + + if (err) { + return Promise.reject(err) + } + + const graph = $rdf.graph() + $rdf.parse(data, graph, 'https://localhost:8443/resources/sampleContainer/', 'text/turtle') + const statements = graph.match(null, ns.ldp('contains'), null) + const files = statements + .map(s => s.object.value.replace(/.*\//, '')) + .map(decodeURIComponent) + + files.sort() + expectedFiles.sort() + assert.deepEqual(files, expectedFiles) + }) }) - }) }) }) }) diff --git a/test/integration/params-test.js b/test/integration/params-test.js index f80ae2674..cd1d1b323 100644 --- a/test/integration/params-test.js +++ b/test/integration/params-test.js @@ -31,7 +31,7 @@ describe('LDNODE params', function () { var server = supertest(ldp) it('should fallback on current working directory', function () { - assert.equal(ldp.locals.ldp.root, process.cwd() + '/') + assert.equal(ldp.locals.ldp.resourceMapper._rootPath, process.cwd()) }) it('should find resource in correct path', function (done) { @@ -56,7 +56,7 @@ describe('LDNODE params', function () { var server = supertest(ldp) it('should fallback on current working directory', function () { - assert.equal(ldp.locals.ldp.root, './test/resources/') + assert.equal(ldp.locals.ldp.resourceMapper._rootPath, path.resolve('./test/resources')) }) it('should find resource in correct path', function (done) { diff --git a/test/integration/special-root-acl-handling.js b/test/integration/special-root-acl-handling.js new file mode 100644 index 000000000..aea464f78 --- /dev/null +++ b/test/integration/special-root-acl-handling.js @@ -0,0 +1,66 @@ +const assert = require('chai').assert +const request = require('request') +const path = require('path') +const { checkDnsSettings, cleanDir } = require('../utils') + +const ldnode = require('../../index') + +const port = 7777 +const serverUri = `https://localhost:${port}` +const root = path.join(__dirname, '../resources/accounts-acl') +const dbPath = path.join(root, 'db') +const configPath = path.join(root, 'config') + +function createOptions (path = '') { + return { + url: `https://nicola.localhost:${port}${path}` + } +} + +describe('Special handling: Root ACL does not give READ access to root', () => { + let ldp, ldpHttpsServer + + before(checkDnsSettings) + + before(done => { + ldp = ldnode.createServer({ + root, + serverUri, + dbPath, + port, + configPath, + sslKey: path.join(__dirname, '../keys/key.pem'), + sslCert: path.join(__dirname, '../keys/cert.pem'), + webid: true, + multiuser: true, + auth: 'oidc', + strictOrigin: true, + host: { serverUri } + }) + ldpHttpsServer = ldp.listen(port, done) + }) + + after(() => { + if (ldpHttpsServer) ldpHttpsServer.close() + cleanDir(root) + }) + + describe('should still grant READ access to everyone because of index.html.acl', () => { + it('for root with /', function (done) { + var options = createOptions('/') + request.get(options, function (error, response, body) { + assert.equal(error, null) + assert.equal(response.statusCode, 200) + done() + }) + }) + it('for root without /', function (done) { + var options = createOptions() + request.get(options, function (error, response, body) { + assert.equal(error, null) + assert.equal(response.statusCode, 200) + done() + }) + }) + }) +}) diff --git a/test/integration/validate-tts.js b/test/integration/validate-tts.js new file mode 100644 index 000000000..1b45ebd92 --- /dev/null +++ b/test/integration/validate-tts.js @@ -0,0 +1,51 @@ +const fs = require('fs') +const path = require('path') +const { setupSupertestServer } = require('../utils') + +const server = setupSupertestServer({ + live: true, + dataBrowserPath: 'default', + root: path.join(__dirname, '../resources'), + auth: 'oidc', + webid: false +}) + +const invalidTurtleBody = fs.readFileSync(path.join(__dirname, '../resources/invalid1.ttl'), { + 'encoding': 'utf8' +}) + +describe('HTTP requests with invalid Turtle syntax', () => { + describe('PUT API', () => { + it('is allowed with invalid TTL files in general', (done) => { + server.put('/invalid1.ttl') + .send(invalidTurtleBody) + .set('content-type', 'text/turtle') + .expect(201, done) + }) + + it('is not allowed with invalid ACL files', (done) => { + server.put('/invalid1.ttl.acl') + .send(invalidTurtleBody) + .set('content-type', 'text/turtle') + .expect(400, done) + }) + }) + + describe('PATCH API', () => { + it('does not support patching of TTL files', (done) => { + server.patch('/patch-1-initial.ttl') + .send(invalidTurtleBody) + .set('content-type', 'text/turtle') + .expect(415, done) + }) + }) + + describe('POST API (multipart)', () => { + it('does not validate files that are posted', (done) => { + server.post('/') + .attach('invalid1', path.join(__dirname, '../resources/invalid1.ttl')) + .attach('invalid2', path.join(__dirname, '../resources/invalid2.ttl')) + .expect(200, done) + }) + }) +}) diff --git a/test/resources/accounts-acl/config/templates/new-account/.acl b/test/resources/accounts-acl/config/templates/new-account/.acl index 35258e69f..1a9dedf54 100644 --- a/test/resources/accounts-acl/config/templates/new-account/.acl +++ b/test/resources/accounts-acl/config/templates/new-account/.acl @@ -13,7 +13,7 @@ acl:accessTo ; # All resources will inherit this authorization, by default - acl:defaultForNew ; + acl:default ; # The owner has all of the access modes allowed acl:mode diff --git a/test/resources/accounts-acl/config/templates/new-account/inbox/.acl b/test/resources/accounts-acl/config/templates/new-account/inbox/.acl index c10554678..17b8e4bb7 100644 --- a/test/resources/accounts-acl/config/templates/new-account/inbox/.acl +++ b/test/resources/accounts-acl/config/templates/new-account/inbox/.acl @@ -10,7 +10,7 @@ <{{webId}}>; acl:accessTo <./>; - acl:defaultForNew <./>; + acl:default <./>; acl:mode acl:Read, acl:Write, acl:Control. diff --git a/test/resources/accounts-acl/config/templates/new-account/profile/card b/test/resources/accounts-acl/config/templates/new-account/profile/card index b9e138ccf..ce337b0a4 100644 --- a/test/resources/accounts-acl/config/templates/new-account/profile/card +++ b/test/resources/accounts-acl/config/templates/new-account/profile/card @@ -18,7 +18,6 @@ solid:account ; # link to the account uri pim:storage ; # root storage - solid:inbox ; ldp:inbox ; pim:preferencesFile ; # private settings/preferences diff --git a/test/resources/accounts-acl/config/templates/new-account/settings/.acl b/test/resources/accounts-acl/config/templates/new-account/settings/.acl index bc871390f..921e65570 100644 --- a/test/resources/accounts-acl/config/templates/new-account/settings/.acl +++ b/test/resources/accounts-acl/config/templates/new-account/settings/.acl @@ -11,7 +11,7 @@ acl:accessTo <./>; # All settings resources will be private, by default, unless overridden - acl:defaultForNew <./>; + acl:default <./>; # The owner has all of the access modes allowed acl:mode diff --git a/test/resources/accounts-acl/config/templates/new-account/settings/privateTypeIndex.ttl b/test/resources/accounts-acl/config/templates/new-account/settings/privateTypeIndex.ttl index 607f6321d..b6fee77e6 100644 --- a/test/resources/accounts-acl/config/templates/new-account/settings/privateTypeIndex.ttl +++ b/test/resources/accounts-acl/config/templates/new-account/settings/privateTypeIndex.ttl @@ -1,4 +1,4 @@ -@prefix solid: . +@prefix solid: . <> a solid:TypeIndex ; a solid:UnlistedDocument. diff --git a/test/resources/accounts-acl/config/templates/new-account/settings/publicTypeIndex.ttl b/test/resources/accounts-acl/config/templates/new-account/settings/publicTypeIndex.ttl index e270db393..433486252 100644 --- a/test/resources/accounts-acl/config/templates/new-account/settings/publicTypeIndex.ttl +++ b/test/resources/accounts-acl/config/templates/new-account/settings/publicTypeIndex.ttl @@ -1,4 +1,4 @@ -@prefix solid: . +@prefix solid: . <> a solid:TypeIndex ; a solid:ListedDocument. diff --git a/test/resources/accounts-acl/config/templates/server/.acl b/test/resources/accounts-acl/config/templates/server/.acl new file mode 100644 index 000000000..05a9842d9 --- /dev/null +++ b/test/resources/accounts-acl/config/templates/server/.acl @@ -0,0 +1,10 @@ +# Root ACL resource for the root +@prefix acl: . +@prefix foaf: . + +<#public> + a acl:Authorization; + acl:agentClass foaf:Agent; # everyone + acl:accessTo ; + acl:default ; + acl:mode acl:Read. diff --git a/test/resources/accounts-acl/localhost/.acl b/test/resources/accounts-acl/localhost/.acl new file mode 100644 index 000000000..05a9842d9 --- /dev/null +++ b/test/resources/accounts-acl/localhost/.acl @@ -0,0 +1,10 @@ +# Root ACL resource for the root +@prefix acl: . +@prefix foaf: . + +<#public> + a acl:Authorization; + acl:agentClass foaf:Agent; # everyone + acl:accessTo ; + acl:default ; + acl:mode acl:Read. diff --git a/test/resources/accounts-acl/nicola.localhost/.acl b/test/resources/accounts-acl/nicola.localhost/.acl new file mode 100644 index 000000000..de881069a --- /dev/null +++ b/test/resources/accounts-acl/nicola.localhost/.acl @@ -0,0 +1 @@ +# This ACL does nothing by default diff --git a/test/resources/accounts-acl/nicola.localhost/index.html b/test/resources/accounts-acl/nicola.localhost/index.html new file mode 100644 index 000000000..cb12e071b --- /dev/null +++ b/test/resources/accounts-acl/nicola.localhost/index.html @@ -0,0 +1 @@ +Everyone should get READ access for this file through
index.html.acl
. diff --git a/test/resources/accounts-acl/nicola.localhost/index.html.acl b/test/resources/accounts-acl/nicola.localhost/index.html.acl new file mode 100644 index 000000000..19f9544f3 --- /dev/null +++ b/test/resources/accounts-acl/nicola.localhost/index.html.acl @@ -0,0 +1,10 @@ +# This file grants everyone READ access to ./index.html + +@prefix acl: . +@prefix foaf: . + +<#public> + a acl:Authorization; + acl:agentClass foaf:Agent; + acl:accessTo <./index.html>; + acl:mode acl:Read. diff --git a/test/resources/accounts-acl/tim.localhost/append-inherited/.acl b/test/resources/accounts-acl/tim.localhost/append-inherited/.acl index 7ef040005..6eb518e41 100644 --- a/test/resources/accounts-acl/tim.localhost/append-inherited/.acl +++ b/test/resources/accounts-acl/tim.localhost/append-inherited/.acl @@ -9,5 +9,5 @@ acl:mode acl:Read, acl:Write, acl:Control; - acl:defaultForNew <./>. + acl:default <./>. diff --git a/test/resources/accounts-acl/tim.localhost/dot-acl/.acl b/test/resources/accounts-acl/tim.localhost/dot-acl/.acl new file mode 100644 index 000000000..447c9114e --- /dev/null +++ b/test/resources/accounts-acl/tim.localhost/dot-acl/.acl @@ -0,0 +1,6 @@ +@prefix acl: . + +<#DotAcl> a acl:Authorization ; + acl:accessTo <./.acl> ; + acl:agent ; + acl:mode acl:Read . diff --git a/test/resources/accounts-acl/tim.localhost/fake-account/.acl b/test/resources/accounts-acl/tim.localhost/fake-account/.acl index f49950774..2f2284163 100644 --- a/test/resources/accounts-acl/tim.localhost/fake-account/.acl +++ b/test/resources/accounts-acl/tim.localhost/fake-account/.acl @@ -1,5 +1,5 @@ <#0> a ; - <./> ; + <./> ; ; , . diff --git a/test/resources/accounts-acl/tim.localhost/group/test-folder/.acl b/test/resources/accounts-acl/tim.localhost/group/test-folder/.acl index e6461a2f2..31d185700 100644 --- a/test/resources/accounts-acl/tim.localhost/group/test-folder/.acl +++ b/test/resources/accounts-acl/tim.localhost/group/test-folder/.acl @@ -1,23 +1,28 @@ @prefix : <#>. @prefix acl: . -@prefix tes: <./>. @prefix c: . @prefix foaf: . :owner a acl:Authorization; - acl:accessTo tes:; + acl:accessTo <./> ; acl:agent c:me; - acl:defaultForNew tes:; + acl:default <./> ; acl:mode acl:Control, acl:Read, acl:Write. -:folks +:public a acl:Authorization; - acl:accessTo tes:; - acl:agentGroup ; - acl:defaultForNew tes:; - acl:mode acl:Read, acl:Write. -:errors + acl:agentClass foaf:Agent; + acl:accessTo <./> ; + acl:defaultForNew <./> ; + acl:mode acl:Read. +:folks a acl:Authorization; - acl:accessTo ; - acl:agentGroup ; + acl:accessTo <./> ; + acl:agentGroup ; + acl:default <./> ; acl:mode acl:Read, acl:Write. +#:errors +# a acl:Authorization; +# acl:accessTo ; +# acl:agentGroup ; +# acl:mode acl:Read, acl:Write. diff --git a/test/resources/accounts-acl/tim.localhost/group/test-folder/group-listing.ttl b/test/resources/accounts-acl/tim.localhost/group/test-folder/group-listing.ttl index e364bb579..29a1f32c1 100644 --- a/test/resources/accounts-acl/tim.localhost/group/test-folder/group-listing.ttl +++ b/test/resources/accounts-acl/tim.localhost/group/test-folder/group-listing.ttl @@ -3,7 +3,7 @@ <> a acl:GroupListing. -<#folks> +<#us> a vcard:Group; vcard:hasUID ; diff --git a/test/resources/accounts-acl/tim.localhost/origin/.acl b/test/resources/accounts-acl/tim.localhost/origin/.acl index 632b670e6..aadd6b41d 100644 --- a/test/resources/accounts-acl/tim.localhost/origin/.acl +++ b/test/resources/accounts-acl/tim.localhost/origin/.acl @@ -1,5 +1,5 @@ <#0> a ; - <./> ; + <./> ; ; , . diff --git a/test/resources/accounts-acl/tim.localhost/owner-only/.acl b/test/resources/accounts-acl/tim.localhost/owner-only/.acl index 632b670e6..aadd6b41d 100644 --- a/test/resources/accounts-acl/tim.localhost/owner-only/.acl +++ b/test/resources/accounts-acl/tim.localhost/owner-only/.acl @@ -1,5 +1,5 @@ <#0> a ; - <./> ; + <./> ; ; , . diff --git a/test/resources/accounts-acl/tim.localhost/read-acl/deeper-tree/.acl b/test/resources/accounts-acl/tim.localhost/read-acl/deeper-tree/.acl new file mode 100644 index 000000000..59ffa5df9 --- /dev/null +++ b/test/resources/accounts-acl/tim.localhost/read-acl/deeper-tree/.acl @@ -0,0 +1,18 @@ +@prefix acl: . + +<#ThisControl> a acl:Authorization ; + acl:accessTo <./> ; + acl:agent ; + acl:mode acl:Control . + +<#DirRead> a acl:Authorization ; + acl:accessTo <./acls-only-on-top/> ; + acl:agent ; + acl:mode acl:Read . + +<#FileRead> a acl:Authorization ; + acl:accessTo <./acls-only-on-top/example.ttl> ; + acl:agent ; + acl:mode acl:Read . + + diff --git a/test/resources/accounts-acl/tim.localhost/read-acl/deeper-tree/acls-only-on-top/example.ttl b/test/resources/accounts-acl/tim.localhost/read-acl/deeper-tree/acls-only-on-top/example.ttl new file mode 100644 index 000000000..512e6aa1d --- /dev/null +++ b/test/resources/accounts-acl/tim.localhost/read-acl/deeper-tree/acls-only-on-top/example.ttl @@ -0,0 +1 @@ +<> a . diff --git a/test/resources/accounts-acl/tim.localhost/write-acl/.acl b/test/resources/accounts-acl/tim.localhost/write-acl/.acl index 632b670e6..1eb861e93 100644 --- a/test/resources/accounts-acl/tim.localhost/write-acl/.acl +++ b/test/resources/accounts-acl/tim.localhost/write-acl/.acl @@ -1,5 +1,6 @@ <#0> a ; - <./> ; + <./> ; + <./> ; ; , . diff --git a/test/resources/accounts-acl/tim.localhost/write-acl/bad-acl-access/.acl b/test/resources/accounts-acl/tim.localhost/write-acl/bad-acl-access/.acl new file mode 100644 index 000000000..aadd6b41d --- /dev/null +++ b/test/resources/accounts-acl/tim.localhost/write-acl/bad-acl-access/.acl @@ -0,0 +1,5 @@ +<#0> + a ; + <./> ; + ; + , . diff --git a/test/resources/accounts-acl/tim.localhost/write-acl/test-file$.txt b/test/resources/accounts-acl/tim.localhost/write-acl/test-file$.txt new file mode 100644 index 000000000..ce002f06a --- /dev/null +++ b/test/resources/accounts-acl/tim.localhost/write-acl/test-file$.txt @@ -0,0 +1 @@ +
. \ No newline at end of file diff --git a/test/resources/accounts-scenario/alice/.acl b/test/resources/accounts-scenario/alice/.acl-override similarity index 100% rename from test/resources/accounts-scenario/alice/.acl rename to test/resources/accounts-scenario/alice/.acl-override diff --git a/test/resources/accounts-scenario/alice/profile/card$.ttl b/test/resources/accounts-scenario/alice/profile/card$.ttl new file mode 100644 index 000000000..a014d960e --- /dev/null +++ b/test/resources/accounts-scenario/alice/profile/card$.ttl @@ -0,0 +1,4 @@ +@prefix acl: . + +<#me> acl:trustedApp [ acl:origin ; + acl:mode acl:Read, acl:Write, acl:Append, acl:Control]. diff --git a/test/resources/accounts-scenario/bob/.acl b/test/resources/accounts-scenario/bob/.acl-override similarity index 91% rename from test/resources/accounts-scenario/bob/.acl rename to test/resources/accounts-scenario/bob/.acl-override index 49a249208..c08da3fff 100644 --- a/test/resources/accounts-scenario/bob/.acl +++ b/test/resources/accounts-scenario/bob/.acl-override @@ -2,4 +2,4 @@ a ; <./>; ; - , , . \ No newline at end of file + , , . diff --git a/test/resources/accounts-strict-origin-off/alice/.acl-override b/test/resources/accounts-strict-origin-off/alice/.acl-override new file mode 100644 index 000000000..bcdd33def --- /dev/null +++ b/test/resources/accounts-strict-origin-off/alice/.acl-override @@ -0,0 +1,5 @@ +<#Owner> + a ; + <./>; + ; + , , . diff --git a/test/resources/accounts-strict-origin-off/alice/db/oidc/op/provider.json b/test/resources/accounts-strict-origin-off/alice/db/oidc/op/provider.json new file mode 100644 index 000000000..68a3b5341 --- /dev/null +++ b/test/resources/accounts-strict-origin-off/alice/db/oidc/op/provider.json @@ -0,0 +1,411 @@ +{ + "issuer": "https://localhost:7010", + "authorization_endpoint": "https://localhost:7010/authorize", + "token_endpoint": "https://localhost:7010/token", + "userinfo_endpoint": "https://localhost:7010/userinfo", + "jwks_uri": "https://localhost:7010/jwks", + "registration_endpoint": "https://localhost:7010/register", + "response_types_supported": [ + "code", + "code token", + "code id_token", + "id_token", + "id_token token", + "code id_token token", + "none" + ], + "response_modes_supported": [ + "query", + "fragment" + ], + "grant_types_supported": [ + "authorization_code", + "implicit", + "refresh_token", + "client_credentials" + ], + "subject_types_supported": [ + "public" + ], + "id_token_signing_alg_values_supported": [ + "RS256", + "RS384", + "RS512", + "none" + ], + "token_endpoint_auth_methods_supported": [ + "client_secret_basic" + ], + "token_endpoint_auth_signing_alg_values_supported": [ + "RS256" + ], + "display_values_supported": [], + "claim_types_supported": [ + "normal" + ], + "claims_supported": [], + "claims_parameter_supported": false, + "request_parameter_supported": true, + "request_uri_parameter_supported": false, + "require_request_uri_registration": false, + "check_session_iframe": "https://localhost:7010/session", + "end_session_endpoint": "https://localhost:7010/logout", + "keys": { + "descriptor": { + "id_token": { + "signing": { + "RS256": { + "alg": "RS256", + "modulusLength": 2048 + }, + "RS384": { + "alg": "RS384", + "modulusLength": 2048 + }, + "RS512": { + "alg": "RS512", + "modulusLength": 2048 + } + }, + "encryption": {} + }, + "token": { + "signing": { + "RS256": { + "alg": "RS256", + "modulusLength": 2048 + }, + "RS384": { + "alg": "RS384", + "modulusLength": 2048 + }, + "RS512": { + "alg": "RS512", + "modulusLength": 2048 + } + }, + "encryption": {} + }, + "userinfo": { + "encryption": {} + }, + "register": { + "signing": { + "RS256": { + "alg": "RS256", + "modulusLength": 2048 + } + } + } + }, + "jwks": { + "keys": [ + { + "kid": "QMa5xaKOYwI", + "kty": "RSA", + "alg": "RS256", + "n": "ol8T6mc7t55kUI3Xf4pHsx8p25VqZ3jm54TQY6xZ07FYzwU8ex02Mg_W_VABRNyVq8wdUBKubo1W8iaKtvtrr0XDOyHUAlwM6xa14332c9akB1AmTirZY4gvyobIY-b18F8LpeIkkLUcypeZmsDd-bGONEJs0sxM6LtLCY41s_lgesPdQwmHLBw4_Rw9NcjBslupWP_pSXUW9x2fj8tKHOoURqmOWL-54t9YbDdIht06uzqagzjPV3UoYtvRsu2QUJx99ExgNvCA9pA1wyiyysyhUdfeKyZpwvTfwWxkrwZrE6zb1AZICFW5R3Bgg0b9UnS96LpFxbSO7rXBRWVKOw", + "e": "AQAB", + "key_ops": [ + "verify" + ], + "ext": true + }, + { + "kid": "66gqKTPVHyA", + "kty": "RSA", + "alg": "RS384", + "n": "rWHV18o2T_9zhO-YknvC9brSjWru496-Dw0iVCiJdEZph94aWXeZW4tYsSjYAfzCJXQUrIqfoPTx100KoYRMiUGpVUh5AgwqAitOpFV_Cxa2j4D01qALwUXWblmJTeCzY7zQFOs1-OBU_U3fhsqcHVPHVlfwO4fYhe-FrhQwTDf55JIW5OGM2bViRxpPyZ3t9nueSuz1jEDCSRlaizJq4jhDrkYWoXtXomRfoseGrZVwTGx5cslfrYs-AmLWMfLkZQWjYZMcwOaJCy3dSkTQkmas4dzGwQqrOqNbgznyCo3oTpHboWS36e0jn_lnbq95zPpZkHNsOzw-Mzq0KRj1jw", + "e": "AQAB", + "key_ops": [ + "verify" + ], + "ext": true + }, + { + "kid": "zPdTDVkjms4", + "kty": "RSA", + "alg": "RS512", + "n": "tbVEZHw4LP21DFHmF0QjbSektUEGl8t4blGcZM871ieppUVZ_uHKlfan2ftnQ4GwBISkmjwk6KP1nc9iiONzqAhJbFheQMT4RAmYz7xPJP9cAPOiD8YbqzO2xJqBT1OMc2PDS7ICH0-t_kBXMb0IR61CTG-CmO5Tp8ecauUkshvvYg0dA3-os16Dy5ex4if3ZSpfXowmfjRrvYAKEPeFTmA9Q8lMzN0ZyuEb2nILIeMFwTAjo8Ck8D1h4keOVaY0mfTgC9KxkR1kIcAc7DELYX-vjG63AwB-8IIo735g5NMrFj22923jymMDpQDTEMui-jW1TQYNGR84ShNW_nJZGQ", + "e": "AQAB", + "key_ops": [ + "verify" + ], + "ext": true + }, + { + "kid": "8zan03XlrmI", + "kty": "RSA", + "alg": "RS256", + "n": "y3E2ffizby9vClbahFgkg8_NcpA6iYTwkA4TH2i2-usyH87nz3uM5LfRa7KIV4ZHOvY-DZr6H1MAqaFCPrciWZnPPyTRxlUlYhfb4n42g0Bwk7Oh7z-KZvIqR45x85NfniR3uB8xu47VdTWCmWJge2glMavdWC8mr5Pc8um3Fr5eMVactUFsp571SrVKuY_u5qJHN3xmn7mLpiLdCIHFlBkvvLgcAcUmXFhn8cCzHCqkrcrRgfjhd5YJmv6wxxINrTMT4_dS4jxE5u1Ezw-m6xSIWukdW21lWupFHNZEK5VvIkiCBl73Sh0vO-gv4XNJcQmPRpqMdHat8GLG7I2PHQ", + "e": "AQAB", + "key_ops": [ + "verify" + ], + "ext": true + }, + { + "kid": "5ooqXseJdoQ", + "kty": "RSA", + "alg": "RS384", + "n": "tqhzPmOds6zvDNX3IHCr7SW5kf6GlnlpW-Rm4aQot1HmtKnsXieV99VjT98EspvcFeKyT5jcJFgZMHb1n_kOtRnhygFHNZQ4ooFmFI2VP5lPUuOIGppWiO5ZxIzECdnwAGoZ7A1hu9Vh5oJJP-m_88JX3sdbHPlsEpzdXHf-THZKAqPYHGNWeNPkmQuON5PaEwiJGRWQ0dbMZtsiMoVCPjh7ZQM-1N2UjXmSAcCCxst5BaTUwQQuVmKIT-cfxP9ShPjP7_DmpRBDOp7_frMKTPkznL0HPNWqSRRewGWB6AcrdY7WtWI5hHNDx9GTdLmolFOvnL4cgtr99u9adq6vAw", + "e": "AQAB", + "key_ops": [ + "verify" + ], + "ext": true + }, + { + "kid": "tfz-MRyl3GQ", + "kty": "RSA", + "alg": "RS512", + "n": "3vyjzgsA89bJHD7BZ2MNadjFOPUXpuGKlC7OumOspC_iOSMnJ0Kh7oCWmsBTuzGdfY_gYMaXx28NK-LVjGmaEKZxPhdCz7yzofUCMWoVXNhE4hwBdZt1mNl0abaKNzJ_8BhbtTNhourfBvRwebMYFYO0LfEkApE-hM929pFERb0G3-_ybw8A2cqQFvQ67JhYsBcFUwpf68xZvfYTbdsJzfkkoY0UgI01GU55dZskPBK-_jndzo_bNo8wdENU_cUdOllNdZUoWAemWOGoHKdNshGKy6OwZaC9xqaE_VzF-RaYEcf_jaLD7GbPrz9Xcm9sygnFV3IDlj9-0fjKOkAwqw", + "e": "AQAB", + "key_ops": [ + "verify" + ], + "ext": true + }, + { + "kid": "FRI7f7sJKUg", + "kty": "RSA", + "alg": "RS256", + "n": "0d4up4hx3l_HZa6D2PO-YYSqgNbAWjtlIbnFWIZS-PRzVFgDKcR9bLo_9-MGm0sT0tHbyeqc4V8o5oeYF0uhNmlEuU9Do2VYiyc0fkNeW1nFrC-XJYxbnge4Pxbhl-0Jn04O8IJsP15xq16GfRZxYfjKPeFilQNeDwTb_SBqoveZASHUW998eO1bEg0bjUZs4Yy23S4R23ABZKTQMPMGMETGFVbcFNXUohu0eSYMG4eU9iaFNmh0p0G9lU7ER-UFSatczaEFle0Awazb7-EHN6iya3zL6LDt-ezlIszTzHIXa2Dhhga2SlDIyuhFUOkkEeCP3a-sFeXre-JWiYz8ZQ", + "e": "AQAB", + "key_ops": [ + "verify" + ], + "ext": true + } + ] + }, + "id_token": { + "signing": { + "RS256": { + "privateJwk": { + "kid": "hS7A2bkdhJA", + "kty": "RSA", + "alg": "RS256", + "n": "ol8T6mc7t55kUI3Xf4pHsx8p25VqZ3jm54TQY6xZ07FYzwU8ex02Mg_W_VABRNyVq8wdUBKubo1W8iaKtvtrr0XDOyHUAlwM6xa14332c9akB1AmTirZY4gvyobIY-b18F8LpeIkkLUcypeZmsDd-bGONEJs0sxM6LtLCY41s_lgesPdQwmHLBw4_Rw9NcjBslupWP_pSXUW9x2fj8tKHOoURqmOWL-54t9YbDdIht06uzqagzjPV3UoYtvRsu2QUJx99ExgNvCA9pA1wyiyysyhUdfeKyZpwvTfwWxkrwZrE6zb1AZICFW5R3Bgg0b9UnS96LpFxbSO7rXBRWVKOw", + "e": "AQAB", + "d": "KvDiH3e1v1XQSc15VG2JkMGJHF1jioHa7xec1P9bsjSQvA8yc4zCLHMr9EoYS9Hac1jD181Y_Du85sVyofnT_MKCYz0LCKS3deTHraY-a1w0fQqhVsLWQxBdMz55-JG4MR2xwukPqoV7W8jEAr1G9yYR6Zg3l3gYTuRZb-mAIlxBZOcZ-4DvB0GtEYLyWFRSMiaUl_EkqgQpssH5hKIIB7xtHZFG_FsbH2gnv0apZFdZCs1hpzyJ_FuUYoIupdaHUP92ojdGQ8E51J_t6SIYjZdtEoKhk6i2BHIPzSerxItVgAs-3RTbeYp1gEdyQGROz8Owyorsqzq-2S7gxXn_AQ", + "p": "zJtqzVrhym0DBWIK11LEIwTPU9G_prC_XYzt0H3tXuiwAlpRoFOoecAKsCJqb_bvL7mqVCjYzS-O57dzdxd36F-vpvW9xH3SgfxcoDtiVkvuoux9nGaQXLWCetjHy8jrL41SDVc55R7h4knn6KgR_yWmY2LR-w0CktrWuIlOIps", + "q": "yyfVYtGaJASgfkv1KL-HoA8yyP7OZrUsxxQcIXMuQoAUqKtvyl6gn2W4uW4opg_yzuN0jxK8ZM8EjOmzXs0_dhVWXWFBBvhmpI4ssXXQYVhJiq-QMm6WvDZFXtW-o3ikRXWuxwnUeOn0psxsIXtHrKQ7Wcf29KrCu64MYTvIoOE", + "dp": "Ptr722ww8wO8KtospGtbr7pZitFjw-yGTVHu-N7GniJzd7WRX-RzXDufzO41roG4hvvNlJ8bwyT6DLsxsIM4Jd3HU04o5wUUNzR0rBCyK_qVq91k1Tg43xIvrFeOQU9O2MPcUhbaxUVQdTjpEnMXXD-PtvZztWEQylaKIhSkOUM", + "dq": "oauPetaGrgcomZlk_gp9qfiuV5m_M6kWe08bm9sHg1BeIGRd0FRByteui5KIsq6acd480eb9GdRNML6CSf1YdWZBlopgr2VaebXTB15UeENEMyPHwvqS5jDiP7glv_2v7L7cWfUOqzKzyVGA85why9fei3JAqzCBS84UeASVgsE", + "qi": "lURMq4viaqzYCi_JF_dDZjvc6rpuiRSzhd0PyY6mdutMZqLtZJESZt8HuEqpey5GA4i481DmmUXCfNhBc-4kUOAP5PVdHJp7YttvrzpkZGtJx_xLwLLLgDn6zaheaeNnxRvHqoW0Cycx6TAAVzX2Cdsjo-vRiaRqGkOBPslOiqA", + "key_ops": [ + "sign" + ], + "ext": true + }, + "publicJwk": { + "kid": "QMa5xaKOYwI", + "kty": "RSA", + "alg": "RS256", + "n": "ol8T6mc7t55kUI3Xf4pHsx8p25VqZ3jm54TQY6xZ07FYzwU8ex02Mg_W_VABRNyVq8wdUBKubo1W8iaKtvtrr0XDOyHUAlwM6xa14332c9akB1AmTirZY4gvyobIY-b18F8LpeIkkLUcypeZmsDd-bGONEJs0sxM6LtLCY41s_lgesPdQwmHLBw4_Rw9NcjBslupWP_pSXUW9x2fj8tKHOoURqmOWL-54t9YbDdIht06uzqagzjPV3UoYtvRsu2QUJx99ExgNvCA9pA1wyiyysyhUdfeKyZpwvTfwWxkrwZrE6zb1AZICFW5R3Bgg0b9UnS96LpFxbSO7rXBRWVKOw", + "e": "AQAB", + "key_ops": [ + "verify" + ], + "ext": true + } + }, + "RS384": { + "privateJwk": { + "kid": "IN7Ur_i2gps", + "kty": "RSA", + "alg": "RS384", + "n": "rWHV18o2T_9zhO-YknvC9brSjWru496-Dw0iVCiJdEZph94aWXeZW4tYsSjYAfzCJXQUrIqfoPTx100KoYRMiUGpVUh5AgwqAitOpFV_Cxa2j4D01qALwUXWblmJTeCzY7zQFOs1-OBU_U3fhsqcHVPHVlfwO4fYhe-FrhQwTDf55JIW5OGM2bViRxpPyZ3t9nueSuz1jEDCSRlaizJq4jhDrkYWoXtXomRfoseGrZVwTGx5cslfrYs-AmLWMfLkZQWjYZMcwOaJCy3dSkTQkmas4dzGwQqrOqNbgznyCo3oTpHboWS36e0jn_lnbq95zPpZkHNsOzw-Mzq0KRj1jw", + "e": "AQAB", + "d": "KtxZqbuiS4phu0YjmGqh2m8xp6X8ojYpE71ydA2FVUomwmCkcOOA9MFwznLDW1JpiCq3BaRLK9YBhvDTpPP4m6Gww5Vj3J63L8wW58b-3fXicX02iWwNG90w0hyuNaNLlWdSpSk0MW77c5bxn_esOg_A4lZg28aMPyfkCxGQkvaHR2EeY9eL9xlZoNtQxtLoKOIl97fJt96C9hXwRPuygDknHywRo35_Fi9nSpdu-_wfoZxBvPvRAQA-jFmwsJwLB3XaYJEZmZoBajXLfYUVEYKCOD7bsPysdbIUCY6dFBAL9FyFl5HSCjgW6yKHql0YfUf5VabQQnkdXDZxng7OiQ", + "p": "2ZOHQkgjfkgMC0Z6gfP-nCj_jqDI2F6MpB4HIvOvpm8z-qz02mZeC6zgHzHtichOwgCPJPapS7hN9vlQdnOTl62XBWmZ2Jbz_q3shbnRPYx7J3GSARrohOHm1bsapxeagQcXoBuScx-Kvgih4B_-u-PHvHFQHHaoEzIpR_FcK-M", + "q": "zABSbkRBSpayejh_y8cvTZI5YZjz8bK_n78yKYOR4bsPrC-c2TGtxsHqv5x_5Ps5ALicul59HgrN8oD8THYJiO2V7wobNIp4HDl2J8aL9me43a2VuDRPOXHeOzxtP071_p6VZWEju0kMDihwn91Vkc2RwrgkNrPClGEQstOg12U", + "dp": "dqxTIEH8qgpeF2BPvcX53-80xPTJ18pqZ7HBLr0_10XcUiQ4QU-IiFG4xQsUhwxseMCZUw3-9UAHtmfyDUmo_Rg_wUICGfLsDsOKuL3LdQjEu5TkkBm_LYuRvo4ZUX48VZgquLeRP526rUBEGt0Ysh7heNVNXtkRf7bicU0iewU", + "dq": "yGvskF7IksJZWNCXZZostBZb_7HowOYvc3_BhZ24vkAs1qr0e0Jei25K9rOHx2y2BSpa-JMKc3CEA6OLvIcfOQLKkk2oiscb2AbwOYXMTmp66ne3J4Fk8HgZsLLeVht6fQxKlI6KL19F08cRV0552kRP1zFYitGy8lomsLpQRC0", + "qi": "scdLHtzFy5A5nhbl6XOv_g_5LxTiebV9ifCo8A_Y8T7hEPsUwOZxkIarcREgDbgo7zdJZjpKEklWrsp6sBT6KyJZf1_e_oZWCoivr7btx05ZWq7bJdn8773y-7DjL3H_HFpqVkpx-f03OKV8_AV-VspkxsVrp0FcrRGs2hpjck0", + "key_ops": [ + "sign" + ], + "ext": true + }, + "publicJwk": { + "kid": "66gqKTPVHyA", + "kty": "RSA", + "alg": "RS384", + "n": "rWHV18o2T_9zhO-YknvC9brSjWru496-Dw0iVCiJdEZph94aWXeZW4tYsSjYAfzCJXQUrIqfoPTx100KoYRMiUGpVUh5AgwqAitOpFV_Cxa2j4D01qALwUXWblmJTeCzY7zQFOs1-OBU_U3fhsqcHVPHVlfwO4fYhe-FrhQwTDf55JIW5OGM2bViRxpPyZ3t9nueSuz1jEDCSRlaizJq4jhDrkYWoXtXomRfoseGrZVwTGx5cslfrYs-AmLWMfLkZQWjYZMcwOaJCy3dSkTQkmas4dzGwQqrOqNbgznyCo3oTpHboWS36e0jn_lnbq95zPpZkHNsOzw-Mzq0KRj1jw", + "e": "AQAB", + "key_ops": [ + "verify" + ], + "ext": true + } + }, + "RS512": { + "privateJwk": { + "kid": "34CvC1I0fbk", + "kty": "RSA", + "alg": "RS512", + "n": "tbVEZHw4LP21DFHmF0QjbSektUEGl8t4blGcZM871ieppUVZ_uHKlfan2ftnQ4GwBISkmjwk6KP1nc9iiONzqAhJbFheQMT4RAmYz7xPJP9cAPOiD8YbqzO2xJqBT1OMc2PDS7ICH0-t_kBXMb0IR61CTG-CmO5Tp8ecauUkshvvYg0dA3-os16Dy5ex4if3ZSpfXowmfjRrvYAKEPeFTmA9Q8lMzN0ZyuEb2nILIeMFwTAjo8Ck8D1h4keOVaY0mfTgC9KxkR1kIcAc7DELYX-vjG63AwB-8IIo735g5NMrFj22923jymMDpQDTEMui-jW1TQYNGR84ShNW_nJZGQ", + "e": "AQAB", + "d": "RZybBe_8yWesu3qIrnMhK0kbtYCi2PZPPdwwEQK_RqzNNg6aiqXPqaHj7gN9LQR6_VAfiyLtdN6TUxDHC_AvN7ls_3_fI-sRvWb7zuGyZFcb1RWBCY_4u57FLw6N6Wj1jqMyh0Y7v-kTnCrj_J8Rk_wRQR0bKCFtlQJIrz73zu90rcPOOjWPH6W6LCN-lkmOC8vm0M08aHJtZclLSJcSO8VX6P4rDbbIadNubhtZ0_snOlc9ZctpWDgqGRqJGKL2FRldmVMnYr9vE4kv3jELCvkKtct35k1q6qd6pQQ2j3iL0nUnIRRCiuIVRmuiUKI11i2lmQoqnzQN0qGNKhAY3Q", + "p": "7psJPwCSpbravp7NlZQWAmvBvZEdPZ3BNk2w7_udpKZqHvnhgwr40jypwp3JdysSXsd4cDKGlzkCaCeJT3L9N26feCGbwbSjwmdf7UcSXDhlfSxAsEI2LtPPkG4SQdsAv42s3NQcPnO9Uf0ZdH-yV8tj5gwB1ibF0_MxtNcenYc", + "q": "wvRiS7lfGGQmJioCYHrkUyeJnwg2-GYbCD6LF2nDXFbsz_JWmgeI1lbSKuD5notoIy62p0R6FdX3mtwb_8dDDHFbjrHfPbOEp-MOd-cZ9f6XxKeC_uoXrkEdcLl5IthG2WtqKT_If5xIMRW4MYwRD3gTUs-DU255B-7YF3oT_F8", + "dp": "jP01QHIRNTatLLffWMRRIQsVRvNpaNA_q9vKcnOmWfUvfbA_rdQc5PWNhf8AXZco2rJZG0rvtomsP2klPLoAn_GQ70ZEp_gaT7QPvcAiPDXi2kz8yGnHZHIqEvjHdvx2e7m2bCI1sj1nn_vDP9B4zGHVXMi5iRYODUiFlYsLfx8", + "dq": "v1uxwDJMLVzgNUW234zA4P5GD1u42Ukf74-0Z17g65pHCDYO21HZ9slxsbTyT7T2mtIXZ0fHrfc_-zliIkYsgeIixIXDYoT7CO_OG9MzoEouHlFuO1j7tYckpqjnRlST9oWAxsxcuSVllwiCq9-YP17VWMtnVFVzZ14BrcmeJHE", + "qi": "4Ufn1YaM5AImU625M0Fd5r9cYU__mPw-HbJHxHoCENGDY8Mz4pB1nJqu10gI5JKgucU4y8VtV1wBzGb_s97gSS8SMmzea6of2SjoxxyR4sYcPfmJz9-1En7F8r3WahBP1IDMTDV6afbTMVPzPIbjkHowmQHyrTBmx2G3rODfStw", + "key_ops": [ + "sign" + ], + "ext": true + }, + "publicJwk": { + "kid": "zPdTDVkjms4", + "kty": "RSA", + "alg": "RS512", + "n": "tbVEZHw4LP21DFHmF0QjbSektUEGl8t4blGcZM871ieppUVZ_uHKlfan2ftnQ4GwBISkmjwk6KP1nc9iiONzqAhJbFheQMT4RAmYz7xPJP9cAPOiD8YbqzO2xJqBT1OMc2PDS7ICH0-t_kBXMb0IR61CTG-CmO5Tp8ecauUkshvvYg0dA3-os16Dy5ex4if3ZSpfXowmfjRrvYAKEPeFTmA9Q8lMzN0ZyuEb2nILIeMFwTAjo8Ck8D1h4keOVaY0mfTgC9KxkR1kIcAc7DELYX-vjG63AwB-8IIo735g5NMrFj22923jymMDpQDTEMui-jW1TQYNGR84ShNW_nJZGQ", + "e": "AQAB", + "key_ops": [ + "verify" + ], + "ext": true + } + } + }, + "encryption": {} + }, + "token": { + "signing": { + "RS256": { + "privateJwk": { + "kid": "oYYAVkAOTqo", + "kty": "RSA", + "alg": "RS256", + "n": "y3E2ffizby9vClbahFgkg8_NcpA6iYTwkA4TH2i2-usyH87nz3uM5LfRa7KIV4ZHOvY-DZr6H1MAqaFCPrciWZnPPyTRxlUlYhfb4n42g0Bwk7Oh7z-KZvIqR45x85NfniR3uB8xu47VdTWCmWJge2glMavdWC8mr5Pc8um3Fr5eMVactUFsp571SrVKuY_u5qJHN3xmn7mLpiLdCIHFlBkvvLgcAcUmXFhn8cCzHCqkrcrRgfjhd5YJmv6wxxINrTMT4_dS4jxE5u1Ezw-m6xSIWukdW21lWupFHNZEK5VvIkiCBl73Sh0vO-gv4XNJcQmPRpqMdHat8GLG7I2PHQ", + "e": "AQAB", + "d": "Jyjwq0jfCSzhO-VCBK2OwcyTKFU431W2lJQQ_D_E0QHK0TKbjmWi1jfH2Ooxsl9HgML7aNUHBlaMzxoaTUYlF4gyy13D_2BOZNZg3fUbeyJBST0Pk6TweCZvGv0BVDhu9FrirI9cnFS_I-Ob8vhrx_VhC3GglqPk13En254_PuIoEuBgTnxRHxNyIqWSiBUQNuEjqDl2CxO1dpjczKbj6YBzbPqFIV8FkJUz6HS2SMhJOgoDhrhduOFwzqqQQ_yUhfPkP2WYodgDXkyUWdgYKuCgCA3hWrQm1YJf-WoN0l_OVvetTSIlbxiNr-Br7PCPBLOpyiGTu4O3LjFHu0cWJQ", + "p": "6AMK_ANmxrnqgQ77Mf6A1BDopCqQWIBBZ4m_Ia4Ia3ygWpKLdjp5NNM1DL5Lnuing7wSNZvj0iyc-Ra_ilYmLeyKROkyPvwoLohk8W6AVPKotvVOZZwK5nHMIN3McSSgileai0FxxZtFG2W-ZLy4ggKQkfsGDnDQ0vWVpcSQ24M", + "q": "4Hn69Uev6x7nlsAV9fR6GvpjUZDD6Np_VY85BtWEw_6VxbutsMJnI0hYqCPMcmSHfdchzEiMLCUaMIRXMvigNdNxTWF1BSL3L3c7A-uZuv0iJ7X3trNmxEeuJ0EBmcqXcukJuYxW-Lb7XzernSGYOGtambMrf1oMM2ER90egyN8", + "dp": "WnKdPKPi1EQ0TH3XpaTO-l1hJGbHgQj29nM3xNyAwc6DAOwyXDPtfo98Fey1tEyEyZFvE-EjDpN_2odownhVR__342d5xB4vV_yqRYjLlNJIvZKiLnCHu1DPwFxXUCB0SZpfUDyCKwQeC9E9SS5dS6nOTGqs8MJAVo2Q-l_IDRs", + "dq": "vM2cCMW87xNVUnRJhE-WjC5hbu9F-Aoe44XNPzRDsN3mdyN92ZgJS6HD_9fsU6K-W1eg88NpMpAaUQhel16K0fr_50e7NxDLuz6bgpac_KuAGMpyiVanbxujHR0ODZ5ad6oOJeQmGpc9Ij8ettDUyU9yKqiKQ1knvec9RzEVyR0", + "qi": "Xamq4vSnrd6jez7YaYdCU6THSykIMrmyHcXjTMrf4WpeAXxH_Wu3F-jPIFy9zTxkZByMwmCqaBqQ_IJmFuqLBvcjpZ_NQmn-wZA4eYuPDnm2ZBhqbzS9b1jhWcAMYeIm0F744vJqqcymqEQnnHYlDyM39_cfrXtmf9gazwglRPA", + "key_ops": [ + "sign" + ], + "ext": true + }, + "publicJwk": { + "kid": "8zan03XlrmI", + "kty": "RSA", + "alg": "RS256", + "n": "y3E2ffizby9vClbahFgkg8_NcpA6iYTwkA4TH2i2-usyH87nz3uM5LfRa7KIV4ZHOvY-DZr6H1MAqaFCPrciWZnPPyTRxlUlYhfb4n42g0Bwk7Oh7z-KZvIqR45x85NfniR3uB8xu47VdTWCmWJge2glMavdWC8mr5Pc8um3Fr5eMVactUFsp571SrVKuY_u5qJHN3xmn7mLpiLdCIHFlBkvvLgcAcUmXFhn8cCzHCqkrcrRgfjhd5YJmv6wxxINrTMT4_dS4jxE5u1Ezw-m6xSIWukdW21lWupFHNZEK5VvIkiCBl73Sh0vO-gv4XNJcQmPRpqMdHat8GLG7I2PHQ", + "e": "AQAB", + "key_ops": [ + "verify" + ], + "ext": true + } + }, + "RS384": { + "privateJwk": { + "kid": "2N9QAEg-94s", + "kty": "RSA", + "alg": "RS384", + "n": "tqhzPmOds6zvDNX3IHCr7SW5kf6GlnlpW-Rm4aQot1HmtKnsXieV99VjT98EspvcFeKyT5jcJFgZMHb1n_kOtRnhygFHNZQ4ooFmFI2VP5lPUuOIGppWiO5ZxIzECdnwAGoZ7A1hu9Vh5oJJP-m_88JX3sdbHPlsEpzdXHf-THZKAqPYHGNWeNPkmQuON5PaEwiJGRWQ0dbMZtsiMoVCPjh7ZQM-1N2UjXmSAcCCxst5BaTUwQQuVmKIT-cfxP9ShPjP7_DmpRBDOp7_frMKTPkznL0HPNWqSRRewGWB6AcrdY7WtWI5hHNDx9GTdLmolFOvnL4cgtr99u9adq6vAw", + "e": "AQAB", + "d": "akN_oCk2AuS1weLsvYuvCe0rk3re88W4fMRY8iadpWDZdftxTql6_s6-0yWsxvgTxwu2rsYANIioRuC-Lw4m90cSa4Ho2ovbkvby2zwvOuvHETLb6JYnh8wan1VBa3XFwYf4grKaTDtslDzxvmQPzxEeK7YRFL8ql3147qXEZNhka8uIo4n-CdbMG0IazaUJp4oTjt9vId_8PDNqc70qGpCE46bh4Ee76GgF6DfWhnHn2rW5uSatjgQbvA7xJ957GVNlA4k-MC3U-S5DOi_cK6YytVdtNIkBY2EaRhtZQQSw0lRvFXxwenaXZsGAbdS9wOhkFfJYiokkxtbdlAkMgQ", + "p": "4-vsibMRfgglnalDkJpUxEJ1Xj4CNzAUr8FZobhqLdhKn1VPNHWIXudmsBuHT6foRLMTTS7R2kzcPWs_c5y8XeaLbETJKSgWtWDjTuBH0J7gg8_th75EInPfhzA79HKfH145DOfpIwMYAxtsDHNvGUjyBwb0Jz6r-at3oSwQncM", + "q": "zSkGydk2AqlUztzU7c2y2KEV8XKuHD8pxloNQEQf42Q9WS7dyYDjKFAfh75QkwHTHxLzzHR4IF83TlYoSslJ7ODiFVa9c_uYScsS9EpOlEtRgiADtlaAkg5o6iCGRPKEKVh9bWZKDcOIe1P-1E_7nw42WcaiHkWhHoV4FWTCVcE", + "dp": "txxX0NkYeJS7A8t1CLu01mg-OxS-WvA57mn8RL0QMPzQFupG0_KJORXXniy_rPNM28SzARNYbXXKi12agJuvihEqejVZF8OpWtcYR8pQZ_78iWmHf8MQok1NjCLoSB--T0k3tHKtDv_xTq29RNOIslu0dojTpqPnLpLfbZElWFk", + "dq": "d5IVtsa1x89Q8k-aeg6M0dzwoQwplaTqgAQz_OS3gRwG9VdvQ-WufuxTtBWjeEpz9YRiOyMWcCeOYEWurgeq4jgfDDjFqjdUho2oumAkdCGBm8l04GyB-p4TX4EdQEnn5QEB-STIvETd4qMNvkfvasApxSETk3kZcmRvnGhhKwE", + "qi": "U38wGRTDA1-NrZFFX1ipttBYz-IFo9VbniwkH0IzQh_yUOKy4iGVvcxcaQyG4X0gaVWrAVi2gUrwZa0a_BCZyX8SNYh7M8EL-9MwL0qmsEwnD6Jq0tvfxFPBll7NyZM5iTLj9I0gWOt1bdcd_WAM3Bm8xOjU8X8L_mpQ4x7WTWo", + "key_ops": [ + "sign" + ], + "ext": true + }, + "publicJwk": { + "kid": "5ooqXseJdoQ", + "kty": "RSA", + "alg": "RS384", + "n": "tqhzPmOds6zvDNX3IHCr7SW5kf6GlnlpW-Rm4aQot1HmtKnsXieV99VjT98EspvcFeKyT5jcJFgZMHb1n_kOtRnhygFHNZQ4ooFmFI2VP5lPUuOIGppWiO5ZxIzECdnwAGoZ7A1hu9Vh5oJJP-m_88JX3sdbHPlsEpzdXHf-THZKAqPYHGNWeNPkmQuON5PaEwiJGRWQ0dbMZtsiMoVCPjh7ZQM-1N2UjXmSAcCCxst5BaTUwQQuVmKIT-cfxP9ShPjP7_DmpRBDOp7_frMKTPkznL0HPNWqSRRewGWB6AcrdY7WtWI5hHNDx9GTdLmolFOvnL4cgtr99u9adq6vAw", + "e": "AQAB", + "key_ops": [ + "verify" + ], + "ext": true + } + }, + "RS512": { + "privateJwk": { + "kid": "b3Miw_cVIr0", + "kty": "RSA", + "alg": "RS512", + "n": "3vyjzgsA89bJHD7BZ2MNadjFOPUXpuGKlC7OumOspC_iOSMnJ0Kh7oCWmsBTuzGdfY_gYMaXx28NK-LVjGmaEKZxPhdCz7yzofUCMWoVXNhE4hwBdZt1mNl0abaKNzJ_8BhbtTNhourfBvRwebMYFYO0LfEkApE-hM929pFERb0G3-_ybw8A2cqQFvQ67JhYsBcFUwpf68xZvfYTbdsJzfkkoY0UgI01GU55dZskPBK-_jndzo_bNo8wdENU_cUdOllNdZUoWAemWOGoHKdNshGKy6OwZaC9xqaE_VzF-RaYEcf_jaLD7GbPrz9Xcm9sygnFV3IDlj9-0fjKOkAwqw", + "e": "AQAB", + "d": "vRW0fWK9Uwe-D7JzZA0NccT8MHk8rikwzskGSe19lth314WkjNkm4Uyu6NjP57uB1dhsJwQf7mCP5bf6548gsp_BBSrKX8ee92YAjxBqvwkxGB8xaZ4C0TMMURwWBgpPjLPLCFNiprjNmGMtATXJ5WyCHDcQFxE813fpY9n477xuCieCuXbZOH0NRwviXncs_TriRFErDj3DFNUxhEnlUJBgIwiAaFNIXTOmLUVDgDX7DBL0XlHEJZ-WfIixHOwGX5WTFlDlPpqE1qXyTLc0yNaS-5P3r4q1ZgDJkEtc5M6cv6uYQmtu0lQmnR22HfuutGIhTBKiHNezpBwQYsPwQQ", + "p": "-R8uHmGlVW5RsOZsWU5dcWIrDgLnwNS4pFVScEwEurQR3JyFt90TZzps8tvpBvqJ3WwgTV38TVlxt31FHQzogJUTvoCmu9kvcHo7cDPKgrbwUdeLuwCQJat-ae2Ay1PHdsGo6l0k32nAwSclurJwHOPesEnYKmVz7GrEcQeakks", + "q": "5SS8OQOsADYKG7upakUXPMzWZVlODOF_vqDaCf1cTTg3Sz84bJsOcyuCy3y1lFepkD3jL7lRXqGFeDVyp8fSQ6O6j9GV2K7FsdTQts7S6zLyG4MmfIaTX97f28gGaMA0Vfp4XghkpJrPZp3vAVXi4vHQfcEqLOv3BfWbtT8s3yE", + "dp": "v4tf1HBTxUobeZ7R9CCy7DkEzbMiKjvk9EW-KyXoBP3rNesLTYGoJ5jeqCS6GWmNkzhN5e67zWVWcDTbbSi9pA_7rGAS0yB8v0jcZrsLnFG4mdTXVSNrsesYOa4pdyylkwP4MqsHbNhNIrZM5i3I0g3K7VT55i83YLH-9MFZ8d0", + "dq": "sr_5ZLCMmI2PTRsTiabj_bX8-Yq07C9sq5RnAqv2OPbFi5mBtpcuqoEiwwJB07qet7rPYwc5hoyRxbhL_L5QNBuhCVgBgMq1xYeyWSj2OCvB2dxxRWrlxD_keVqMRWLKcNe21gPOHun1KKPDMlOBbnAcqDzrXaelR6zVDop9woE", + "qi": "cl2oEYi-L2OYO5oyXJK9Kc-Oj2098Tx4o2WbfIZXQeOO48SMz7iRa03FOmh-b0G8WrtGxm7pVWIbryvBVlaLJYEhughgR1vtiSd_nJ5XCxDAF9p8h3hEQrnr8rmO0HTNE3z9PqBmVOCKvgbzOOa9S7YL3LgMMdCrs-CZLNJixio", + "key_ops": [ + "sign" + ], + "ext": true + }, + "publicJwk": { + "kid": "tfz-MRyl3GQ", + "kty": "RSA", + "alg": "RS512", + "n": "3vyjzgsA89bJHD7BZ2MNadjFOPUXpuGKlC7OumOspC_iOSMnJ0Kh7oCWmsBTuzGdfY_gYMaXx28NK-LVjGmaEKZxPhdCz7yzofUCMWoVXNhE4hwBdZt1mNl0abaKNzJ_8BhbtTNhourfBvRwebMYFYO0LfEkApE-hM929pFERb0G3-_ybw8A2cqQFvQ67JhYsBcFUwpf68xZvfYTbdsJzfkkoY0UgI01GU55dZskPBK-_jndzo_bNo8wdENU_cUdOllNdZUoWAemWOGoHKdNshGKy6OwZaC9xqaE_VzF-RaYEcf_jaLD7GbPrz9Xcm9sygnFV3IDlj9-0fjKOkAwqw", + "e": "AQAB", + "key_ops": [ + "verify" + ], + "ext": true + } + } + }, + "encryption": {} + }, + "userinfo": { + "encryption": {} + }, + "register": { + "signing": { + "RS256": { + "privateJwk": { + "kid": "xaDu9yTDJwA", + "kty": "RSA", + "alg": "RS256", + "n": "0d4up4hx3l_HZa6D2PO-YYSqgNbAWjtlIbnFWIZS-PRzVFgDKcR9bLo_9-MGm0sT0tHbyeqc4V8o5oeYF0uhNmlEuU9Do2VYiyc0fkNeW1nFrC-XJYxbnge4Pxbhl-0Jn04O8IJsP15xq16GfRZxYfjKPeFilQNeDwTb_SBqoveZASHUW998eO1bEg0bjUZs4Yy23S4R23ABZKTQMPMGMETGFVbcFNXUohu0eSYMG4eU9iaFNmh0p0G9lU7ER-UFSatczaEFle0Awazb7-EHN6iya3zL6LDt-ezlIszTzHIXa2Dhhga2SlDIyuhFUOkkEeCP3a-sFeXre-JWiYz8ZQ", + "e": "AQAB", + "d": "MMLZvi0yn8NLNfHdSnwfgNLtNrAu0wg30CU0mAPlpVhOr4sfeQXhSnDfyf2inFROT85YrcpoFukgPKfGi6sT0uuUfyXOhllWwEYkqS2H79uk1QPdr2i0JLyQb4AmfEEj2jKtv-3drr-H05RL1SZww52lh8klOZAlu4Gah26PuDs5Oy4VcgdiiNFIaBAd1WzCmDxW7xd-tMc709s76eWXCa1F4ZJJny3uGCtJGVBUnfrao6d_RJNw4BX7pje5hYcwSFnZN3AiMmzzje4CP6JU7lSpMImwqOtKhIHbFEGSTfnCb4bPbwpQc0IBYaUlTrWCsXDQgYkNdsLaFiVdsvGWgQ", + "p": "7ynGMOB3sQXZ7OoW051wDUdsByiwx1_j7ecpLZ1rSTZ7-TCERMXQq0okQHkVpx6AlU8tOromhMxd_yGFEPOIHC7ZEm4o-f0-DzkDO6RPCCvhli0IaxhaGHHh0r4jAhQf7IlaTkdWt_x1wLxUTgIeS3FUiR_I3SvgeX0oBnTCPuE", + "q": "4KRx-LfdeI_Po7scXMqwbYobUgUdHIwGMKhrnerG_Kfv6b8K7PWNjfrjPYS9MR4OM1_9fs3I9xvr-Dc9oBNFU4AYn5bgTdxiPxLqwPemkKD-R4PswCEFn6bNqec7rDx_a0139SUjGTpLmHJEBk6GxESxc4cmy7B3uH0hpLnEAgU", + "dp": "GerO_XD1x4s4yIG9o4UGJYo93LC-J3zuuFM6lSTgAXkUCjcFvmYpbMaGEy6L-CZIDOYdoqWVkwKKlSFOyD173IH_KA4kBqM43HFzNj1iCyKmOZ37sY9cBBYjQLE2Hf2MUBe6X40_ioSjRhWilSmJmudXH9z_6Wfd1YyIj9qLSkE", + "dq": "V7R8duG6Sx7Cr6cW7LeJ6Ep6Xix28DVPGWI4GgCWzf_3MCiRaHB6YstoOxV_FBUq8C3y97X6V1gEafWPTtcXQwGJG425Z0y64utM0igeVOs2O8-q-FwsKOfj9gi8iINaMBef1Qs5x9i1uQArxNaV0T0MlxFWOUJdi573KqwIdc0", + "qi": "4WRsDyw9pg2iIva4THIhvg0eda_5wu-gorBCQRh5cxtJ4DwMV4GfTL-PUP6u8HRhR8Ns3XLbOk2pAfft9aPTCI9wr_ke1NUyupR1EzGOEekdfHtpgQP06hU7nhSMFbj9OMQvrw4_3k_5BXHQXsp7wqS7QtawQDWbS5BKc6O4nZA", + "key_ops": [ + "sign" + ], + "ext": true + }, + "publicJwk": { + "kid": "FRI7f7sJKUg", + "kty": "RSA", + "alg": "RS256", + "n": "0d4up4hx3l_HZa6D2PO-YYSqgNbAWjtlIbnFWIZS-PRzVFgDKcR9bLo_9-MGm0sT0tHbyeqc4V8o5oeYF0uhNmlEuU9Do2VYiyc0fkNeW1nFrC-XJYxbnge4Pxbhl-0Jn04O8IJsP15xq16GfRZxYfjKPeFilQNeDwTb_SBqoveZASHUW998eO1bEg0bjUZs4Yy23S4R23ABZKTQMPMGMETGFVbcFNXUohu0eSYMG4eU9iaFNmh0p0G9lU7ER-UFSatczaEFle0Awazb7-EHN6iya3zL6LDt-ezlIszTzHIXa2Dhhga2SlDIyuhFUOkkEeCP3a-sFeXre-JWiYz8ZQ", + "e": "AQAB", + "key_ops": [ + "verify" + ], + "ext": true + } + } + } + }, + "jwkSet": "{\"keys\":[{\"kid\":\"QMa5xaKOYwI\",\"kty\":\"RSA\",\"alg\":\"RS256\",\"n\":\"ol8T6mc7t55kUI3Xf4pHsx8p25VqZ3jm54TQY6xZ07FYzwU8ex02Mg_W_VABRNyVq8wdUBKubo1W8iaKtvtrr0XDOyHUAlwM6xa14332c9akB1AmTirZY4gvyobIY-b18F8LpeIkkLUcypeZmsDd-bGONEJs0sxM6LtLCY41s_lgesPdQwmHLBw4_Rw9NcjBslupWP_pSXUW9x2fj8tKHOoURqmOWL-54t9YbDdIht06uzqagzjPV3UoYtvRsu2QUJx99ExgNvCA9pA1wyiyysyhUdfeKyZpwvTfwWxkrwZrE6zb1AZICFW5R3Bgg0b9UnS96LpFxbSO7rXBRWVKOw\",\"e\":\"AQAB\",\"key_ops\":[\"verify\"],\"ext\":true},{\"kid\":\"66gqKTPVHyA\",\"kty\":\"RSA\",\"alg\":\"RS384\",\"n\":\"rWHV18o2T_9zhO-YknvC9brSjWru496-Dw0iVCiJdEZph94aWXeZW4tYsSjYAfzCJXQUrIqfoPTx100KoYRMiUGpVUh5AgwqAitOpFV_Cxa2j4D01qALwUXWblmJTeCzY7zQFOs1-OBU_U3fhsqcHVPHVlfwO4fYhe-FrhQwTDf55JIW5OGM2bViRxpPyZ3t9nueSuz1jEDCSRlaizJq4jhDrkYWoXtXomRfoseGrZVwTGx5cslfrYs-AmLWMfLkZQWjYZMcwOaJCy3dSkTQkmas4dzGwQqrOqNbgznyCo3oTpHboWS36e0jn_lnbq95zPpZkHNsOzw-Mzq0KRj1jw\",\"e\":\"AQAB\",\"key_ops\":[\"verify\"],\"ext\":true},{\"kid\":\"zPdTDVkjms4\",\"kty\":\"RSA\",\"alg\":\"RS512\",\"n\":\"tbVEZHw4LP21DFHmF0QjbSektUEGl8t4blGcZM871ieppUVZ_uHKlfan2ftnQ4GwBISkmjwk6KP1nc9iiONzqAhJbFheQMT4RAmYz7xPJP9cAPOiD8YbqzO2xJqBT1OMc2PDS7ICH0-t_kBXMb0IR61CTG-CmO5Tp8ecauUkshvvYg0dA3-os16Dy5ex4if3ZSpfXowmfjRrvYAKEPeFTmA9Q8lMzN0ZyuEb2nILIeMFwTAjo8Ck8D1h4keOVaY0mfTgC9KxkR1kIcAc7DELYX-vjG63AwB-8IIo735g5NMrFj22923jymMDpQDTEMui-jW1TQYNGR84ShNW_nJZGQ\",\"e\":\"AQAB\",\"key_ops\":[\"verify\"],\"ext\":true},{\"kid\":\"8zan03XlrmI\",\"kty\":\"RSA\",\"alg\":\"RS256\",\"n\":\"y3E2ffizby9vClbahFgkg8_NcpA6iYTwkA4TH2i2-usyH87nz3uM5LfRa7KIV4ZHOvY-DZr6H1MAqaFCPrciWZnPPyTRxlUlYhfb4n42g0Bwk7Oh7z-KZvIqR45x85NfniR3uB8xu47VdTWCmWJge2glMavdWC8mr5Pc8um3Fr5eMVactUFsp571SrVKuY_u5qJHN3xmn7mLpiLdCIHFlBkvvLgcAcUmXFhn8cCzHCqkrcrRgfjhd5YJmv6wxxINrTMT4_dS4jxE5u1Ezw-m6xSIWukdW21lWupFHNZEK5VvIkiCBl73Sh0vO-gv4XNJcQmPRpqMdHat8GLG7I2PHQ\",\"e\":\"AQAB\",\"key_ops\":[\"verify\"],\"ext\":true},{\"kid\":\"5ooqXseJdoQ\",\"kty\":\"RSA\",\"alg\":\"RS384\",\"n\":\"tqhzPmOds6zvDNX3IHCr7SW5kf6GlnlpW-Rm4aQot1HmtKnsXieV99VjT98EspvcFeKyT5jcJFgZMHb1n_kOtRnhygFHNZQ4ooFmFI2VP5lPUuOIGppWiO5ZxIzECdnwAGoZ7A1hu9Vh5oJJP-m_88JX3sdbHPlsEpzdXHf-THZKAqPYHGNWeNPkmQuON5PaEwiJGRWQ0dbMZtsiMoVCPjh7ZQM-1N2UjXmSAcCCxst5BaTUwQQuVmKIT-cfxP9ShPjP7_DmpRBDOp7_frMKTPkznL0HPNWqSRRewGWB6AcrdY7WtWI5hHNDx9GTdLmolFOvnL4cgtr99u9adq6vAw\",\"e\":\"AQAB\",\"key_ops\":[\"verify\"],\"ext\":true},{\"kid\":\"tfz-MRyl3GQ\",\"kty\":\"RSA\",\"alg\":\"RS512\",\"n\":\"3vyjzgsA89bJHD7BZ2MNadjFOPUXpuGKlC7OumOspC_iOSMnJ0Kh7oCWmsBTuzGdfY_gYMaXx28NK-LVjGmaEKZxPhdCz7yzofUCMWoVXNhE4hwBdZt1mNl0abaKNzJ_8BhbtTNhourfBvRwebMYFYO0LfEkApE-hM929pFERb0G3-_ybw8A2cqQFvQ67JhYsBcFUwpf68xZvfYTbdsJzfkkoY0UgI01GU55dZskPBK-_jndzo_bNo8wdENU_cUdOllNdZUoWAemWOGoHKdNshGKy6OwZaC9xqaE_VzF-RaYEcf_jaLD7GbPrz9Xcm9sygnFV3IDlj9-0fjKOkAwqw\",\"e\":\"AQAB\",\"key_ops\":[\"verify\"],\"ext\":true},{\"kid\":\"FRI7f7sJKUg\",\"kty\":\"RSA\",\"alg\":\"RS256\",\"n\":\"0d4up4hx3l_HZa6D2PO-YYSqgNbAWjtlIbnFWIZS-PRzVFgDKcR9bLo_9-MGm0sT0tHbyeqc4V8o5oeYF0uhNmlEuU9Do2VYiyc0fkNeW1nFrC-XJYxbnge4Pxbhl-0Jn04O8IJsP15xq16GfRZxYfjKPeFilQNeDwTb_SBqoveZASHUW998eO1bEg0bjUZs4Yy23S4R23ABZKTQMPMGMETGFVbcFNXUohu0eSYMG4eU9iaFNmh0p0G9lU7ER-UFSatczaEFle0Awazb7-EHN6iya3zL6LDt-ezlIszTzHIXa2Dhhga2SlDIyuhFUOkkEeCP3a-sFeXre-JWiYz8ZQ\",\"e\":\"AQAB\",\"key_ops\":[\"verify\"],\"ext\":true}]}" + } +} \ No newline at end of file diff --git a/test/resources/accounts-strict-origin-off/alice/private-for-alice.txt b/test/resources/accounts-strict-origin-off/alice/private-for-alice.txt new file mode 100644 index 000000000..3dd4d7a1a --- /dev/null +++ b/test/resources/accounts-strict-origin-off/alice/private-for-alice.txt @@ -0,0 +1 @@ +protected contents for alice diff --git a/test/resources/accounts-strict-origin-off/alice/private-for-alice.txt.acl b/test/resources/accounts-strict-origin-off/alice/private-for-alice.txt.acl new file mode 100644 index 000000000..337ba077a --- /dev/null +++ b/test/resources/accounts-strict-origin-off/alice/private-for-alice.txt.acl @@ -0,0 +1,12 @@ +<#Alice> + a ; + + <./private-for-alice.txt>; + + # Alice web id + ; + + + , + , + . diff --git a/test/resources/accounts-strict-origin-off/alice/profile/card$.ttl b/test/resources/accounts-strict-origin-off/alice/profile/card$.ttl new file mode 100644 index 000000000..a014d960e --- /dev/null +++ b/test/resources/accounts-strict-origin-off/alice/profile/card$.ttl @@ -0,0 +1,4 @@ +@prefix acl: . + +<#me> acl:trustedApp [ acl:origin ; + acl:mode acl:Read, acl:Write, acl:Append, acl:Control]. diff --git a/test/resources/accounts-strict-origin-off/bob/.acl-override b/test/resources/accounts-strict-origin-off/bob/.acl-override new file mode 100644 index 000000000..13df41ce6 --- /dev/null +++ b/test/resources/accounts-strict-origin-off/bob/.acl-override @@ -0,0 +1,5 @@ +<#Owner> + a ; + <./>; + ; + , , . diff --git a/test/resources/accounts-strict-origin-off/bob/db/oidc/op/provider.json b/test/resources/accounts-strict-origin-off/bob/db/oidc/op/provider.json new file mode 100644 index 000000000..d77cc8409 --- /dev/null +++ b/test/resources/accounts-strict-origin-off/bob/db/oidc/op/provider.json @@ -0,0 +1,411 @@ +{ + "issuer": "https://localhost:7011", + "authorization_endpoint": "https://localhost:7011/authorize", + "token_endpoint": "https://localhost:7011/token", + "userinfo_endpoint": "https://localhost:7011/userinfo", + "jwks_uri": "https://localhost:7011/jwks", + "registration_endpoint": "https://localhost:7011/register", + "response_types_supported": [ + "code", + "code token", + "code id_token", + "id_token", + "id_token token", + "code id_token token", + "none" + ], + "response_modes_supported": [ + "query", + "fragment" + ], + "grant_types_supported": [ + "authorization_code", + "implicit", + "refresh_token", + "client_credentials" + ], + "subject_types_supported": [ + "public" + ], + "id_token_signing_alg_values_supported": [ + "RS256", + "RS384", + "RS512", + "none" + ], + "token_endpoint_auth_methods_supported": [ + "client_secret_basic" + ], + "token_endpoint_auth_signing_alg_values_supported": [ + "RS256" + ], + "display_values_supported": [], + "claim_types_supported": [ + "normal" + ], + "claims_supported": [], + "claims_parameter_supported": false, + "request_parameter_supported": true, + "request_uri_parameter_supported": false, + "require_request_uri_registration": false, + "check_session_iframe": "https://localhost:7011/session", + "end_session_endpoint": "https://localhost:7011/logout", + "keys": { + "descriptor": { + "id_token": { + "signing": { + "RS256": { + "alg": "RS256", + "modulusLength": 2048 + }, + "RS384": { + "alg": "RS384", + "modulusLength": 2048 + }, + "RS512": { + "alg": "RS512", + "modulusLength": 2048 + } + }, + "encryption": {} + }, + "token": { + "signing": { + "RS256": { + "alg": "RS256", + "modulusLength": 2048 + }, + "RS384": { + "alg": "RS384", + "modulusLength": 2048 + }, + "RS512": { + "alg": "RS512", + "modulusLength": 2048 + } + }, + "encryption": {} + }, + "userinfo": { + "encryption": {} + }, + "register": { + "signing": { + "RS256": { + "alg": "RS256", + "modulusLength": 2048 + } + } + } + }, + "jwks": { + "keys": [ + { + "kid": "QXv-I1cn_YU", + "kty": "RSA", + "alg": "RS256", + "n": "siMc_WZIcylWwv6_0mtLKKkH-jDLIsREie9KRannw_BxVvM96CaFlUHywVu1Nkyw_hqCYFXyr91JnQ7YN_fB7OpcBcx6rjbExlD_piewy2X_vXq5_0YN5a3aPb5P6JSmRF-nS257jdteucmEVJeFHzSqXAU2K5Y_2ZXse9hGTg9MJMnzof4jntb19q02E0qBOGeBqDoMXQU0RJB2uncqUzXp7EJeB4UVwvcS107LTYwPX7BAHwyGorpgWc_DYutZq-4DxycQgvIlO3_hGiFjaWhVOHHkO1DGcWTGOM9DcFels7Abf_XkCPIrgeALlOaqUe6dnK-yFCGwBeJhZupP_Q", + "e": "AQAB", + "key_ops": [ + "verify" + ], + "ext": true + }, + { + "kid": "TxrWAv0g4Yk", + "kty": "RSA", + "alg": "RS384", + "n": "7d5w1iTgbqF5L8wLqFxS72vXfhmOCRHxOzoxFuAJKDtgceguvIZKcA8Krlm4qE10_Fjm3v2BhTBqKaMWeMMqM2KPCeOOm_APcOEqHLltmU301ErcEU7XJFH90HFJVlPDMbuPvX1_0mi904J-TNQ_XX1s4d1OBgJbEn-4RfongbIQK2Fv1LRclZ3n1__dVikrnngSZEWazdidEMFDlI5KNJv6jbGdQAfPzLWzq_9Fl-wbmGu4Up6COLeQDdGybmPCv3a0id9-jVlyZRauUc2RtO3mJdQaOUZNOuDFe2WT0xmhPhoLXKEWhlF1udJGzm0beY7s0pwg4QjhGi7T75d3lQ", + "e": "AQAB", + "key_ops": [ + "verify" + ], + "ext": true + }, + { + "kid": "-tvrpKoUfPU", + "kty": "RSA", + "alg": "RS512", + "n": "sGoD0dZ3-F4NaAKQQrPhSGAbkBvJ0OsA3PQiwAhWXMrdOj0n9o2QDQfICdayIHcRwsisQaOz7B-L-pYcYFgMcdhj-yWOYVUMHXJCTqJeiz4zdGqjlCxkOF9ebauahRk_IoXp_hDyx3hqB98giajTn1gpW9G36Hcau4PutfM_jeyxi4iVeQSm8-p7qLCm-qD7ot0vzCqvsIt9cDvm9o5DVwG-LoAzeyZlEDmuDpFrLsFLz-D9rxLfSSdAethycrDA0Fhe1j7Az15NYUItJIkvyM5AsC6X0xyjO1LJWoJfbl9cbfaJQ0c3XKRc_NuxPYcDuM15gw0FaTkgUOYezuALPQ", + "e": "AQAB", + "key_ops": [ + "verify" + ], + "ext": true + }, + { + "kid": "DA4MU9NJoDo", + "kty": "RSA", + "alg": "RS256", + "n": "vaugR-F0yLkwgUyJPrIxjUrncPFoGlFyfPWYfVlU4OleYqNnEofoUNI2afIqRQlZgMgfxhknAi6IEhQKdNQGOAqzKoBxEvVK30x4XxMyVPMYYWbTCEEX2K5Kna0wJQ39rsajBWBRusxge_bLydt0dun4ZX4lv_BaCkdKn4TB6x3sQtwPyYrkOj7nykBiLzpcNGYKYbL5B5HGJHbcjXqX6ttvUn7B7E_idcyJbFfcwzrSj87Zdp5aj5LSiIqHtih8Sm2Mch5VQDT_U55UT4n8Rc9K_fJ1bQirXQdZZ9CgU4zcjXx8PnaEpeq18EcXxXsdfCN8o4EBUN7YWog1-65CLw", + "e": "AQAB", + "key_ops": [ + "verify" + ], + "ext": true + }, + { + "kid": "re686ijoKyE", + "kty": "RSA", + "alg": "RS384", + "n": "p3SLWJT-PyQn6n-fvj_s5dNqRJQauAZCC8JFnwUUP6n_LUfCG5tpfRLzZ0yXtsaPfN-z9c8JbXIbsF0HJN3NLTaBOWaZ04yjl3S29_8y-KwOMKlOafuRbYy7sXkicvCnjXRIcV85pAewrpwwHBcI2wDPDbkflwI3m6nurThQAQBDxPWz83QnC_iQ_V3pJGoGYItUGH_sJC06mqATV05dVzHSS0VZSFhEoyEtpykZNnhzgZCOACi4V8P07y0jsxQtqdPYYpqt9qg0Tjp81r-tIIDY7LEWv949rbR3Oad-QBcQC0f4CEapT2nbcVcadODtqPv1pQzSL7sSJ42zATGSxQ", + "e": "AQAB", + "key_ops": [ + "verify" + ], + "ext": true + }, + { + "kid": "jGE5qoPDlnI", + "kty": "RSA", + "alg": "RS512", + "n": "-WuAFX1DLbsMHagMw_NDJA6yxCcIm2SDgbMmDKOwCN4dR0YWdwKzDJ31EeoakDg-T85HaU48R4Kcx1WQUYnly98vDymMFl4Xe6zS9je-Uh3IVfkYJV3pwVKr2gj1hrX7QglIktLg0o0iWulFe1K1NQ5u7Jbe7ZZhOaetlTFH8KUgS78fajfhcFtrXBliqxHTiBz2lMYCj2ntqX8K8ue3_lVLBXm53AJG3T-ymnUd-nxJ-NsdFMSIzQO1-KUona9OgxDpu1cFoQ-YzOteSz2OP58wDl3gL5urIhzK7Hvu3I3okzlL2TyCHmOhLn6HMHr2-mwEgsYYnsEQwzu6bbY8hw", + "e": "AQAB", + "key_ops": [ + "verify" + ], + "ext": true + }, + { + "kid": "PhN1R2mze2k", + "kty": "RSA", + "alg": "RS256", + "n": "yGhTYJWPKU-n1gnPQPf1_Nr8HBJ6F-IT6PyxLJoURWrd_DvuW0EFQD1JolKAYWakuJAIWGtb8CXjRdzXrEVYNfshb-bBGYGotISxAD2z_KzEtcBya_TwywAQiMYildsAPRZR7HqvxnUw2iFUwec_gw6aBvu3rPcLnj6z2KKDJDFC1P4LHNDArh4EquhkgtzeD5h6-8cCI1iKpb-Op5nCe1DjUNg49L5NZhNtDlhCEb8P2LEbcV8vldIz9gJu0MSf94zzWu2JGsVtVQ29XPO8SQqrKQ5TKClmR1jIIGCF7t4AUoaaL6AA81F3cyEhKB3vN_D8DzEsr7tuwomLFZXmBw", + "e": "AQAB", + "key_ops": [ + "verify" + ], + "ext": true + } + ] + }, + "id_token": { + "signing": { + "RS256": { + "privateJwk": { + "kid": "jYjEMK74CQI", + "kty": "RSA", + "alg": "RS256", + "n": "siMc_WZIcylWwv6_0mtLKKkH-jDLIsREie9KRannw_BxVvM96CaFlUHywVu1Nkyw_hqCYFXyr91JnQ7YN_fB7OpcBcx6rjbExlD_piewy2X_vXq5_0YN5a3aPb5P6JSmRF-nS257jdteucmEVJeFHzSqXAU2K5Y_2ZXse9hGTg9MJMnzof4jntb19q02E0qBOGeBqDoMXQU0RJB2uncqUzXp7EJeB4UVwvcS107LTYwPX7BAHwyGorpgWc_DYutZq-4DxycQgvIlO3_hGiFjaWhVOHHkO1DGcWTGOM9DcFels7Abf_XkCPIrgeALlOaqUe6dnK-yFCGwBeJhZupP_Q", + "e": "AQAB", + "d": "EDX50WWZekFsz9n3AuMYt96KB06hbcyGoDbSf3xyxvX-mncQTywSQ_74ZHhACWz5PmaTLskjiLWOmWhcbNtzHmhFb5GoKGp7hcChQzGheSETIN8mRgDOCvTDQ5MqGnRMnTRHOSoYvFQWTrnz_O69ApX8WttujpDhxp9KIgsQetHqnBohh2MWuDZ3aauxR9jdeeoZUGzRpqOgQxqXRNP7V0mHC6KrsClfbeq1jipMEW2EShopmWZdn_k7cnrKL2Aoo_Lox3XQhTnAcNiBj-l9kzls629lCoCarPQCZZaaV64NumhRqORUZ7QlG7csQrRHuxzT54ljbERRGiZfNlZ1UQ", + "p": "5C2Emcxi3hQ4wJOr7Vqr4nvaE0MY1TsBpfomG7h-59VDYFb11zcV4HIG1hfUd6LTwaYqMGmtS1_WFt9S-wuKZyJ1F64p76ywBOgXi6IArsV2uxG24HHO-mPoefeQU8eQtbbPkyJXZcCZJuNFlwPEP9y_NsV0ExDgVZr82U6qcpc", + "q": "x9uYjcsbRF6c2d3Ogb6Jcc3pfm0bocelg1tKfV87urYOFJuT-0ZYN-wPgj9l-IH1sobwhraTTCir0tkrBr77pUTQHO30B3eOScPUDl5p9NMIFpg1agl6tPp7z4jM7rPrnsFh0UjuGt5jHQGpDPQCNDpE5_DukuxCUxm04giNqIs", + "dp": "jRO3kH_WAQjvreGfwzj0XSvGQXKSwAOjmUN4nFsN27j313DsvwvH8uWNZIGHVBDQbEKYgyZThu7SJ4IchCs2f13Gl8WPGCjlC6OUKzkWwvhD2JWzREIZfqaW7hIqoiIZTsCgxo_NCZRzHKAYPq6NgA60CuE6Sy98BHG3M4R3zjE", + "dq": "OK6MLrLK8fIPdC7XMa7zVkZ7EXMqYhC4XW_XyYTn8MVPawLQznQd8wZNQ7htWDSrlU12DA7d6byjNrKG5Gvn_PBuQbYu_qsmvL8Adm7KiDgN1DKo-4Div09HLA31aUG38peQAYY4mYA3BfQBmP3fXiakgk-vqhW9ncntpimc248", + "qi": "raxY35RjgNgbTeIbBzD3Jm0Xz9oCW_HMf8sA3ruvx3erRqhVTeWEn3IpVwYGs_mjxO1IPGzBpGHSTTwObTiDU2Bgv8msniQHcMuld-fye50Rq1K4GBnCsv5frYM2Bw5Z3V5cTsda2zG_QczqV84gu6-vezRcLZyoWFMRXkng6hg", + "key_ops": [ + "sign" + ], + "ext": true + }, + "publicJwk": { + "kid": "QXv-I1cn_YU", + "kty": "RSA", + "alg": "RS256", + "n": "siMc_WZIcylWwv6_0mtLKKkH-jDLIsREie9KRannw_BxVvM96CaFlUHywVu1Nkyw_hqCYFXyr91JnQ7YN_fB7OpcBcx6rjbExlD_piewy2X_vXq5_0YN5a3aPb5P6JSmRF-nS257jdteucmEVJeFHzSqXAU2K5Y_2ZXse9hGTg9MJMnzof4jntb19q02E0qBOGeBqDoMXQU0RJB2uncqUzXp7EJeB4UVwvcS107LTYwPX7BAHwyGorpgWc_DYutZq-4DxycQgvIlO3_hGiFjaWhVOHHkO1DGcWTGOM9DcFels7Abf_XkCPIrgeALlOaqUe6dnK-yFCGwBeJhZupP_Q", + "e": "AQAB", + "key_ops": [ + "verify" + ], + "ext": true + } + }, + "RS384": { + "privateJwk": { + "kid": "JXnna97ZRPY", + "kty": "RSA", + "alg": "RS384", + "n": "7d5w1iTgbqF5L8wLqFxS72vXfhmOCRHxOzoxFuAJKDtgceguvIZKcA8Krlm4qE10_Fjm3v2BhTBqKaMWeMMqM2KPCeOOm_APcOEqHLltmU301ErcEU7XJFH90HFJVlPDMbuPvX1_0mi904J-TNQ_XX1s4d1OBgJbEn-4RfongbIQK2Fv1LRclZ3n1__dVikrnngSZEWazdidEMFDlI5KNJv6jbGdQAfPzLWzq_9Fl-wbmGu4Up6COLeQDdGybmPCv3a0id9-jVlyZRauUc2RtO3mJdQaOUZNOuDFe2WT0xmhPhoLXKEWhlF1udJGzm0beY7s0pwg4QjhGi7T75d3lQ", + "e": "AQAB", + "d": "l-OKObOifAIv2A17BC6v5qH-IE9BGxmR6DfoUKsOLdp8Yz_XWBqIPbXdJCe2egG7ycca5RkLM3kO0TeKieJVeCpfG_lqfrhzo6ijs2PUUCgvRb_ndkXZqx-P6yHrqmwiIAecaWtRy5GGZSDWdUiyjYghlrgxsjSe4tkbWdO6ll1aNvByRPiN50iy_oIWpW2hR9zt3gJmgrhJKpimZUi4HErZtS9xoUkp-9lP4cAgTs4YqhfGI6ID1dBVsvua7ouFCZqz8ldB0Tqw2qqvwnpDe6REFLe7dSPv6yzOBrTPdyALyCl391T-teiWXLFRDpONYaoJm8ATIJ_ScEncGgqeAQ", + "p": "-oXU6Sz1EwDLGD7D0ct4SC0zIN7fvmpJSEyL_1NlewO0BfZoMr8-gum4yRttBKbqjzWvfecGWNqXSlhuoTiEfBav76vn9Ah9CTUbWXQhz7-kebT72SB2quzib-0NUzFlHB6kyepUOXZiwbhTuAbY5OF0QcudVwVLs_rJMUVugRE", + "q": "8xHJJ2-MRn_Jys6xnS_ujzvWvyUVksDYUwkzcyaXR9gQSx1r7N9FPiye-r6r-6Z7T9jSiwCDzmQeu9O4oBlmLdDb7PdYweo8K_sbIIS8gdxg3fkqX3VPPGtHcRZUM6OE1euAabfo_a_SDDXo9wU2w7POpVCjYJYUqRWhwS3DzkU", + "dp": "dtN1yme9kpbkvgo_PUpCMhHmV2f_PkURn05XdPKshq2Z_N4ETFWzo8qSECmHCxbU2LqBE7m5o_mCmwvY5XV2OZlVN9wU-AXysKRU5ZgU1YIz1FqIvlMMlkQnnykUEeqy56SNSwxviJXjf7kbVIVC6UUarH6UkYr8flCsj0c8g9E", + "dq": "OcX7csUfqU4MT4kLTFotMJw_cZVF6GivwiTIttDwWiIv1Tq8AUQcOCfw7ZZOWHT8kEDv4hwQOAkyCODM3DjNQYyICW3NCeI7xRRdIUCLca5I09m6SZAfcjPpeiadgtyV4SalkfslhM66dS6HHdd-acueDUr6WiWx6XJ7zOesx6U", + "qi": "vvHbpuUf7IvZqsHBXXKL75yA6pEOzm_klHRoafrku40DHbddXD68MSBzsifTMyEbzNNwj2N6YygWoF7OhXbl2rET2PQrDJUBrdvoaUKdg7MqUPHKASlRt_OUV81Tt5dR-UcIvZ8JnsjR_HTC5FZd5uXvxn8VLztUhKYm1UBbVtQ", + "key_ops": [ + "sign" + ], + "ext": true + }, + "publicJwk": { + "kid": "TxrWAv0g4Yk", + "kty": "RSA", + "alg": "RS384", + "n": "7d5w1iTgbqF5L8wLqFxS72vXfhmOCRHxOzoxFuAJKDtgceguvIZKcA8Krlm4qE10_Fjm3v2BhTBqKaMWeMMqM2KPCeOOm_APcOEqHLltmU301ErcEU7XJFH90HFJVlPDMbuPvX1_0mi904J-TNQ_XX1s4d1OBgJbEn-4RfongbIQK2Fv1LRclZ3n1__dVikrnngSZEWazdidEMFDlI5KNJv6jbGdQAfPzLWzq_9Fl-wbmGu4Up6COLeQDdGybmPCv3a0id9-jVlyZRauUc2RtO3mJdQaOUZNOuDFe2WT0xmhPhoLXKEWhlF1udJGzm0beY7s0pwg4QjhGi7T75d3lQ", + "e": "AQAB", + "key_ops": [ + "verify" + ], + "ext": true + } + }, + "RS512": { + "privateJwk": { + "kid": "iArDjqR9QcE", + "kty": "RSA", + "alg": "RS512", + "n": "sGoD0dZ3-F4NaAKQQrPhSGAbkBvJ0OsA3PQiwAhWXMrdOj0n9o2QDQfICdayIHcRwsisQaOz7B-L-pYcYFgMcdhj-yWOYVUMHXJCTqJeiz4zdGqjlCxkOF9ebauahRk_IoXp_hDyx3hqB98giajTn1gpW9G36Hcau4PutfM_jeyxi4iVeQSm8-p7qLCm-qD7ot0vzCqvsIt9cDvm9o5DVwG-LoAzeyZlEDmuDpFrLsFLz-D9rxLfSSdAethycrDA0Fhe1j7Az15NYUItJIkvyM5AsC6X0xyjO1LJWoJfbl9cbfaJQ0c3XKRc_NuxPYcDuM15gw0FaTkgUOYezuALPQ", + "e": "AQAB", + "d": "Tuh8oOVvcBaRpI5Q_KT9BaSHb6QeV2ZmUm6ZBJA2IPdUkPI959hWMJ3kahIwRrk7poagFhQlLF7H--Qc-TMpDdsejX20-_BQpPMwmX-jDmFaHp58YJCim1x9Hkz9pr8uMED58vydu38u3ip9oVV0oveKOFnMCx0LRgizQ4t0SARyvQRUTcI1_MzVufsSLO_fI_eZFN4xyxEXp-oCajhBxyf7AsYfg1LLPsziZNXU8Ept40E6MrCWII7BmZ9wKsmgr9HpPwGFr55X8EEGPtXgwVRLQ1zyQvQ65mD0vuhRiNvSFZInTrSGouRIwDW-jXM4ThuISPhfnQYDA8HZNVrsQQ", + "p": "4pLFRcSFgT3IjTvKS112P7o84e5VY1mJnnMZh-oPTMTrY5-gdTfbhiYR0c7hLJllclsNPLie3kai53yh7jQbs-CTdv6ZxFzmPwv_NADwm5RMSF5EfVhu0USFyI4h85b4FWbBAh7dI4ajI-I8uS3XDqyy_ENZ6wFenMsRKDHZRik", + "q": "x1OGU2g3yID1bFk54RJvAha2cSTWy2T48IiNHva-req_RDraNDdKZOr1QDEnnAaYxP0oT80ETSbfpC5bmU56ZuRA-aZUwiugWqMU-w_A5Oe8NtEH1X05iE3sOPzjV8wtqomgRdRDmwLHmwygV5cWEOBnoiZ0lke8nI28iY_xdvU", + "dp": "YOzwv3vmsPdBSEn_rGX7JCAD05MlrC6tlL8geOhES08ic6fh-MNAgg7fKKYb5fxpTZZt_z_rlqMZJXZHv6NDBpxX-VvJZBtp1CbJsfGE_MlgKMVK_2RJY_SwVq-XDqHS1zTx9HpHl98NA1jRKVbW3Uw79XbKjKI1W1XzrQJGNKE", + "dq": "cVbEDY84EPGLG9XMfHdv2Z8ylDlfTX4XsyXiIJYrLFdL9K8GPiSmT6XuWFba7_QsT-6nSmEILhqJliCqAw1FulXVwF7c2R_XaVJL7soxY7eFJSJMsw8mdKPiSzE40EaQPOVO1gXxfyXgfAB89_E4IdaH9wKozn7x9478grfvlDU", + "qi": "o6vR6kC1LTU_6D81fEAf9tXSc-01yuvWxIHc_mRcbI-h-H7BRXBgZrj5X7j3ttPe8g3aG13FZ6y8GpNSrs4LgqNc2VGhxlV8FAV_gVnbgGkpnQaUVtecaHgz03mHOH5hBWOmqBCOLqS4De1KcrwZ1bmhinVu24FVY04Tq82Dc2k", + "key_ops": [ + "sign" + ], + "ext": true + }, + "publicJwk": { + "kid": "-tvrpKoUfPU", + "kty": "RSA", + "alg": "RS512", + "n": "sGoD0dZ3-F4NaAKQQrPhSGAbkBvJ0OsA3PQiwAhWXMrdOj0n9o2QDQfICdayIHcRwsisQaOz7B-L-pYcYFgMcdhj-yWOYVUMHXJCTqJeiz4zdGqjlCxkOF9ebauahRk_IoXp_hDyx3hqB98giajTn1gpW9G36Hcau4PutfM_jeyxi4iVeQSm8-p7qLCm-qD7ot0vzCqvsIt9cDvm9o5DVwG-LoAzeyZlEDmuDpFrLsFLz-D9rxLfSSdAethycrDA0Fhe1j7Az15NYUItJIkvyM5AsC6X0xyjO1LJWoJfbl9cbfaJQ0c3XKRc_NuxPYcDuM15gw0FaTkgUOYezuALPQ", + "e": "AQAB", + "key_ops": [ + "verify" + ], + "ext": true + } + } + }, + "encryption": {} + }, + "token": { + "signing": { + "RS256": { + "privateJwk": { + "kid": "mBFgdSiTnAQ", + "kty": "RSA", + "alg": "RS256", + "n": "vaugR-F0yLkwgUyJPrIxjUrncPFoGlFyfPWYfVlU4OleYqNnEofoUNI2afIqRQlZgMgfxhknAi6IEhQKdNQGOAqzKoBxEvVK30x4XxMyVPMYYWbTCEEX2K5Kna0wJQ39rsajBWBRusxge_bLydt0dun4ZX4lv_BaCkdKn4TB6x3sQtwPyYrkOj7nykBiLzpcNGYKYbL5B5HGJHbcjXqX6ttvUn7B7E_idcyJbFfcwzrSj87Zdp5aj5LSiIqHtih8Sm2Mch5VQDT_U55UT4n8Rc9K_fJ1bQirXQdZZ9CgU4zcjXx8PnaEpeq18EcXxXsdfCN8o4EBUN7YWog1-65CLw", + "e": "AQAB", + "d": "A-MDetWc7gwVeWDXIyjFqS6SxZa82mU24maqBE-TVLSTkZPlpdSRJy7XnJ4wzY9efSwcspOLYBkSAsTUXgaGRhm6CDHvn0LVkPPhN5mOG32Lz1srEe07jt4re0W0Sd4ah71cU9zgb-KGS6QIEw_jOBidVX8bSO6k-bbySYiP7MB8I5HM0P_bpD8Xin7pOj6QCrU3BksG0Ql_dHz1B0npvpBzK-UAwkFlBoryL59BQeN65A5f0MjSlBhsVFoXVt-2GMy-SbyY8WJI90YCY6F66LfO9RuaEVkOfL9Efux_UwB03LPKQW4gnmjx8MLMfIz_eGYplVo1Gr2R-5qeufySwQ", + "p": "9lyv_3EC0ihvXZ7Rmv2wXrqmlzb1qGKmx4MGk03IkGijGXdVyCIZke9O5gkGHnrxlFsl8VUy08u-FfW8fe7lpiFhp32QnImOEjRAb1gih_fRN16MWOUFDq14jcuVqS1A3Ce_wsai7QlwdAXx6J2HYvqdFqT1JvCji1N1Z-oefGE", + "q": "xRcsLk-icb1q7xXOhWaSIdUBQ5sxwwC8Y9n5PfSaYuoR4YNxSTp0-WzDQ_h9OigfKJBL6Nzd0_scIys4FKZ3EkxXXMM0ksi3oks2sP8Nws0wRWPRxvQSIWSxD28OH3cpJPc-u-d0p7YagBzlb1PkeZ7CcklO9XQeMGbW401byI8", + "dp": "BCZrkJIGyiOEPL-AKGw9nFrok4OJf9ypkpLSeYjF6CjbFoK8HzLz21F8ssNUJw9LIoSmjvowcQDtotEQ684qcDH_wyKKXRi0G_plW3rQmhnCnHwrQRQakbS6YykazE7G2O6SfGV8OSH_kvTGrnR442H1Y3xD5PQIzUAKqkV3XgE", + "dq": "jqMUoHeVswdJCrsXecgf3khP3-PDgcNYlFM-ZK5PxWJtim4cYMjju1gRgXGm_53l3u9_YInoKPBFDtbKgXdwGVgSqdnEhK6q59PTQwlzphXl53I0ce5V6MD8u5S9_du5dT1Ss57w-Cd8ylcfXRCz-6kARMH1WQKujoz-3AYydNU", + "qi": "Dcb0uVC4XaXFMFV4ansHRGnp4y5zZNuK-5cQQftLfzKVgZw3AYELw6ZYgQfLWZ_tim9TtVDc62wNQJ4KEUTb0TvsScuRhQNigQABfGDx_cfkllGx6JnDmn1x7kpLKbglhZVw0IrBBL9OiklwF4r7LnXVKP-toerHCUgbJyiv6s0", + "key_ops": [ + "sign" + ], + "ext": true + }, + "publicJwk": { + "kid": "DA4MU9NJoDo", + "kty": "RSA", + "alg": "RS256", + "n": "vaugR-F0yLkwgUyJPrIxjUrncPFoGlFyfPWYfVlU4OleYqNnEofoUNI2afIqRQlZgMgfxhknAi6IEhQKdNQGOAqzKoBxEvVK30x4XxMyVPMYYWbTCEEX2K5Kna0wJQ39rsajBWBRusxge_bLydt0dun4ZX4lv_BaCkdKn4TB6x3sQtwPyYrkOj7nykBiLzpcNGYKYbL5B5HGJHbcjXqX6ttvUn7B7E_idcyJbFfcwzrSj87Zdp5aj5LSiIqHtih8Sm2Mch5VQDT_U55UT4n8Rc9K_fJ1bQirXQdZZ9CgU4zcjXx8PnaEpeq18EcXxXsdfCN8o4EBUN7YWog1-65CLw", + "e": "AQAB", + "key_ops": [ + "verify" + ], + "ext": true + } + }, + "RS384": { + "privateJwk": { + "kid": "DCo68-1L_bA", + "kty": "RSA", + "alg": "RS384", + "n": "p3SLWJT-PyQn6n-fvj_s5dNqRJQauAZCC8JFnwUUP6n_LUfCG5tpfRLzZ0yXtsaPfN-z9c8JbXIbsF0HJN3NLTaBOWaZ04yjl3S29_8y-KwOMKlOafuRbYy7sXkicvCnjXRIcV85pAewrpwwHBcI2wDPDbkflwI3m6nurThQAQBDxPWz83QnC_iQ_V3pJGoGYItUGH_sJC06mqATV05dVzHSS0VZSFhEoyEtpykZNnhzgZCOACi4V8P07y0jsxQtqdPYYpqt9qg0Tjp81r-tIIDY7LEWv949rbR3Oad-QBcQC0f4CEapT2nbcVcadODtqPv1pQzSL7sSJ42zATGSxQ", + "e": "AQAB", + "d": "peGcfDa0GaZeaDze6J74tL6WobK5ORzx5iYfw6RLZ7mmD5vrHF_6wqy4M9c63xOZZpFP-yuJ4kJMTYwKHKofqy84Gb2ammbSU6GJ8ud5_b6rG-dLx08uw__KmsctqgDdahIMBUrYlbYMfxw3yEvFOPV3JtgBBB1tKqXOywdisWmQgk54eBwA_3Mmm-Flt_a2PiibDjivVUAWxbBPd8Ug9y2bPbDiAXoMP6pOAT317LUqBqw7dhie9_aQWKAgOo2dGjBUADnCb51-Nr8syTJRpczYc9ahYlYOlk3miv3aEhXjQ9a7QosRxvqAMPtjVK2OgB0cQ2j46rzbSrZoYcogzQ", + "p": "z-mDM_oSHM4dhR4LAGXgKRdCmTIzBnGBCQ5Szg0yRTxlXPCqF8NrIu7mhkVB61JiGAHqgD5Y9IxPJyWj9uW9qrY21kIwuIEcEi-mTdxp4VgAjP6vaG1XOaS_fdshBfqe43seTzqHJF523vGmkF8HfOAUqhbj9bsTlCCQBBquQiM", + "q": "zi-ToMifbZweWX_HXu0XRMgLZ1vHYWPjgWdddAJD5wCHx_K6lRROhvTCHycV6f9Qrd-0jnauMRNnHJWlCuU0AGQo9CI-mKAnj66LpvMtfyA5i7WK89-ll5EEWXV5NFdXJd9c7d9mmQAXFQssC3mfPcq7EvGjnAMg_eGSFlLF4fc", + "dp": "bmxQEa_0JLZXuVaOc0SoPEqtRV4C-Z4Y8S1ZTzR1CY6dKzJqtDpG0YPejVuFOi1ECgoieMAkUKWgeGMmZT_5bwxdrYf0BloUBZinE91HorYxfLDbinPgCq50Qay7KkjEUH8YRu4HzooZSik_1JeUC3-bmgaURfN434g31OYyvM8", + "dq": "BzueBybiiNrQKb2Uxdy1U0FdPQv4K49wfKqemaI2tZTMLpRyPSaQNqXBZYxedW3ya5cMY28AX1JZ7KPHPWGa-GSLFz1YSsxbduvdlEa1kt8ThbYhoLp4uZ9psqWvUcm6keaLAQE0PSvGo4NX0TM8BdPlyh1V6vQlJSLTmBrhPDM", + "qi": "L5c_XlyLw4tTUWdrM730pvhC2eGCdSwCc6XAwnUla6q3klxJV1eNcRZoejHAN_QXLS742gpA9A_PTMBfYuBN3-lNLOe-wB3bUDJtCzwGlTthiHChkuqB0KAdq4oj8X49ZMA7Fqlr2KaTFTBsItC-ScVSbss9W2DZv8-XzyNoMyc", + "key_ops": [ + "sign" + ], + "ext": true + }, + "publicJwk": { + "kid": "re686ijoKyE", + "kty": "RSA", + "alg": "RS384", + "n": "p3SLWJT-PyQn6n-fvj_s5dNqRJQauAZCC8JFnwUUP6n_LUfCG5tpfRLzZ0yXtsaPfN-z9c8JbXIbsF0HJN3NLTaBOWaZ04yjl3S29_8y-KwOMKlOafuRbYy7sXkicvCnjXRIcV85pAewrpwwHBcI2wDPDbkflwI3m6nurThQAQBDxPWz83QnC_iQ_V3pJGoGYItUGH_sJC06mqATV05dVzHSS0VZSFhEoyEtpykZNnhzgZCOACi4V8P07y0jsxQtqdPYYpqt9qg0Tjp81r-tIIDY7LEWv949rbR3Oad-QBcQC0f4CEapT2nbcVcadODtqPv1pQzSL7sSJ42zATGSxQ", + "e": "AQAB", + "key_ops": [ + "verify" + ], + "ext": true + } + }, + "RS512": { + "privateJwk": { + "kid": "0XGSMsImQPQ", + "kty": "RSA", + "alg": "RS512", + "n": "-WuAFX1DLbsMHagMw_NDJA6yxCcIm2SDgbMmDKOwCN4dR0YWdwKzDJ31EeoakDg-T85HaU48R4Kcx1WQUYnly98vDymMFl4Xe6zS9je-Uh3IVfkYJV3pwVKr2gj1hrX7QglIktLg0o0iWulFe1K1NQ5u7Jbe7ZZhOaetlTFH8KUgS78fajfhcFtrXBliqxHTiBz2lMYCj2ntqX8K8ue3_lVLBXm53AJG3T-ymnUd-nxJ-NsdFMSIzQO1-KUona9OgxDpu1cFoQ-YzOteSz2OP58wDl3gL5urIhzK7Hvu3I3okzlL2TyCHmOhLn6HMHr2-mwEgsYYnsEQwzu6bbY8hw", + "e": "AQAB", + "d": "fO-4TQtd5z5Wp1RScKUd8KXcLh1PVmdW6FUQriwgNZDtIZKeFicoAR3ucHbPr4Y80EUHyFwEHd3zInZdwDpO_XxiWjn5jgq7wJulYOgzUXbRrx8DVVRhjxEWPDVYp43ouf9kdwdizpUbrGZFA60-T8FXFvPL3z8AnJy9eoog-wz-otG58fosrd2Zdpo3O5PlnN2HzueP7G2vabmiZcwR7WEhSaBcI-HcFCXSdZcnAeVzv2DMOoNLNNQXOQ-DtAF_-7lrZa2LKbJccPMEYybwXJlGpRB0JTMYtiLWeZY0EWkWiG2p-lBFsDisEH29JS6yBsG30WWLR55e2XwCqBp1gQ", + "p": "_xiXor89SL9DcybJIEcg_a9nlmnS0DPRfwlRL7fMDF_YKRCVN9x-DD6LlnDss8x9NnUB69dg17QYgLGHI1DbfpV30eOP95zswUfhoyRRgGyxCVbyfB_xwnLTsyCNE3Koi33KjNSMNmDdGKIGBKGzs7Lnbms93Ylr-m9nPizBffc", + "q": "-k3CSpMNknvsVgDxcyl9-U6P2Gm3COcL2NeRzCNR4sG-ffw9xJ_NGIqps9Oddx3Vu5tlEHYiDG3cy-4yOkzxWhphS8E_u8Q1wmBUBaepDjOthoXUcGaSr7-CY7d_FH_ou54WS6rN7diNJODdWATgOSlbPFNhc-he3M8U_bwI0fE", + "dp": "M9zAF2ph_0RDZkngYhuT7X-Xw9DH92RRl05Bnz3y0iE6RT8F11GQntSodHGI7hUI7-Vh-pzTJ4eJ48A0BU4PEfE5Zwao4mKZD8KZcR2VJFL0uz1eFzY3ZJ0LxUM861NISPPOFkuwJe4ThUqLhq2JZ2NcAerzrPKfbU7w4oce34k", + "dq": "mc9OpSTYIjukbvUFag8FKj3shr_vibjwvr85CIhruTv1ItXt3vWTwpDy114iVSAwRqim4ga1xY19MJOeqdS-OvAa-cI2t9tKbbdj6lWsvN1ktFVoxelCGl1EcqI_pQk0qpXqfXToNk_r682CNqRIZNfVCKz0JZWVNXrLG2CAywE", + "qi": "upS-Z2wbbZ_Mla0QpAlORgIr3QLBTK5W0hkhRahoAqm2aGzq269FGSIBnJNxxVkFXL4koHu7diwVCdBHOn3RtnoFYRV1V0sO52Q0B8KI0M8iHM4mVYbk80TsSYBU4lYJqy7U7OnZj1pfnrV4Ky2AAvhTjp2ZbpJCqEPRGJqQlP8", + "key_ops": [ + "sign" + ], + "ext": true + }, + "publicJwk": { + "kid": "jGE5qoPDlnI", + "kty": "RSA", + "alg": "RS512", + "n": "-WuAFX1DLbsMHagMw_NDJA6yxCcIm2SDgbMmDKOwCN4dR0YWdwKzDJ31EeoakDg-T85HaU48R4Kcx1WQUYnly98vDymMFl4Xe6zS9je-Uh3IVfkYJV3pwVKr2gj1hrX7QglIktLg0o0iWulFe1K1NQ5u7Jbe7ZZhOaetlTFH8KUgS78fajfhcFtrXBliqxHTiBz2lMYCj2ntqX8K8ue3_lVLBXm53AJG3T-ymnUd-nxJ-NsdFMSIzQO1-KUona9OgxDpu1cFoQ-YzOteSz2OP58wDl3gL5urIhzK7Hvu3I3okzlL2TyCHmOhLn6HMHr2-mwEgsYYnsEQwzu6bbY8hw", + "e": "AQAB", + "key_ops": [ + "verify" + ], + "ext": true + } + } + }, + "encryption": {} + }, + "userinfo": { + "encryption": {} + }, + "register": { + "signing": { + "RS256": { + "privateJwk": { + "kid": "RiiZpS0yli8", + "kty": "RSA", + "alg": "RS256", + "n": "yGhTYJWPKU-n1gnPQPf1_Nr8HBJ6F-IT6PyxLJoURWrd_DvuW0EFQD1JolKAYWakuJAIWGtb8CXjRdzXrEVYNfshb-bBGYGotISxAD2z_KzEtcBya_TwywAQiMYildsAPRZR7HqvxnUw2iFUwec_gw6aBvu3rPcLnj6z2KKDJDFC1P4LHNDArh4EquhkgtzeD5h6-8cCI1iKpb-Op5nCe1DjUNg49L5NZhNtDlhCEb8P2LEbcV8vldIz9gJu0MSf94zzWu2JGsVtVQ29XPO8SQqrKQ5TKClmR1jIIGCF7t4AUoaaL6AA81F3cyEhKB3vN_D8DzEsr7tuwomLFZXmBw", + "e": "AQAB", + "d": "AlSEM6lJgtd0Qh7XFBBOAeSh7cmhNcnhJWJaxQP9nFkDv70KpnD5GKgdxQ9kgr1oyOST1ENbTE2EePl6YHxl7CrBVsW-FQfW6FJqpHATOarglqRoMC0m55VWm-CB4nArbopl5XP_uzT9nmuoyqBfsqxmyhH_LrmonuxntusczzzmFY-0vmdE8JHQcnixa70dLd9GO4Bs6vr1SfO5bb8FFEFNm00iTKZqOnqDLx0abgzcqYA_wDBxznFg3RtwdOrVOzKsIUgAaO9C6s5XzGS3ddmggEkPGTm0kQQ9xHJkYOxMNZPNRloqMqVpy1O4JbleCMzPg_-DQ8MJzAWXy5MgkQ", + "p": "_-nK2E0JvckScJUcFH9eN0pJUymgAWMH2_MjE_qJb_lT4jXHLcIfzafqu1QxH2ZbHH1V-Pu2HBwk2YMpS-MQheSSrSNXo4s8Kf4nswspClVVpZnMh8pWWLiHDbib7wMhj8cUlKYTM91J6sAooicGh_8UJtFoSlyTn0sLRSG1iUU", + "q": "yHm3dqNtMfujFwNF9yz5Tb8cDgcDQBxox-y6QGWwogmWP2QOfX76QaBYg32Ve6WUwttixu6KK127BkvGeCReC_QaFILLwPZzI7ybQStRzKNiQg-9oki5lw7iQz66O8U1rQ8jHwSy6mC-Ka3CP1v2j3sZMwmzoWSGBuEwZHfvGNs", + "dp": "MeJ55vYFaxhR3t5CDgDR9ccmSe64QOzz8D0R3mgc-FXKszK2c8X-exE7YW7E8JD8O6523sT6N7qIuFmn6CIH64Gl0dgs0jVm8eyYAn-vbVM7Eb_MLcAUWtEFings9UdP-H3fFibVYWvGZ9szUIB49nOTkwmI4c3v_MIdHUm34F0", + "dq": "lRjZFpYC7yqMJ-BaV7OOEoFwtwoGI9c9IntwJda7YDerE6gxkqouK0K6schjtVr3YVhShUsBXbFPGCahC9dYTqGUB-8i5HcmOMzb1sMGCiD1QdZ9HcXiqXL7WjG7xhosinH4l-ghvGiI5WyCTCb0H8_YdPnhK8YcW9984yvDmgM", + "qi": "UUZvIyomeQkGmtQH41T3vZV_HZSzcZEmdFYPgmjiZMXkOrB3wM1FkqOnoDF51yD8dCuTLSL-Pcq6523NTKtvRLYkum0aZFGMyfPHnK6nGbdsKxjGtHgUuccOUX9XL-WoEsn0F2UGc39JNFQNcScoaIAtgGJ5A5rjfpfvIK7c-Vo", + "key_ops": [ + "sign" + ], + "ext": true + }, + "publicJwk": { + "kid": "PhN1R2mze2k", + "kty": "RSA", + "alg": "RS256", + "n": "yGhTYJWPKU-n1gnPQPf1_Nr8HBJ6F-IT6PyxLJoURWrd_DvuW0EFQD1JolKAYWakuJAIWGtb8CXjRdzXrEVYNfshb-bBGYGotISxAD2z_KzEtcBya_TwywAQiMYildsAPRZR7HqvxnUw2iFUwec_gw6aBvu3rPcLnj6z2KKDJDFC1P4LHNDArh4EquhkgtzeD5h6-8cCI1iKpb-Op5nCe1DjUNg49L5NZhNtDlhCEb8P2LEbcV8vldIz9gJu0MSf94zzWu2JGsVtVQ29XPO8SQqrKQ5TKClmR1jIIGCF7t4AUoaaL6AA81F3cyEhKB3vN_D8DzEsr7tuwomLFZXmBw", + "e": "AQAB", + "key_ops": [ + "verify" + ], + "ext": true + } + } + } + }, + "jwkSet": "{\"keys\":[{\"kid\":\"QXv-I1cn_YU\",\"kty\":\"RSA\",\"alg\":\"RS256\",\"n\":\"siMc_WZIcylWwv6_0mtLKKkH-jDLIsREie9KRannw_BxVvM96CaFlUHywVu1Nkyw_hqCYFXyr91JnQ7YN_fB7OpcBcx6rjbExlD_piewy2X_vXq5_0YN5a3aPb5P6JSmRF-nS257jdteucmEVJeFHzSqXAU2K5Y_2ZXse9hGTg9MJMnzof4jntb19q02E0qBOGeBqDoMXQU0RJB2uncqUzXp7EJeB4UVwvcS107LTYwPX7BAHwyGorpgWc_DYutZq-4DxycQgvIlO3_hGiFjaWhVOHHkO1DGcWTGOM9DcFels7Abf_XkCPIrgeALlOaqUe6dnK-yFCGwBeJhZupP_Q\",\"e\":\"AQAB\",\"key_ops\":[\"verify\"],\"ext\":true},{\"kid\":\"TxrWAv0g4Yk\",\"kty\":\"RSA\",\"alg\":\"RS384\",\"n\":\"7d5w1iTgbqF5L8wLqFxS72vXfhmOCRHxOzoxFuAJKDtgceguvIZKcA8Krlm4qE10_Fjm3v2BhTBqKaMWeMMqM2KPCeOOm_APcOEqHLltmU301ErcEU7XJFH90HFJVlPDMbuPvX1_0mi904J-TNQ_XX1s4d1OBgJbEn-4RfongbIQK2Fv1LRclZ3n1__dVikrnngSZEWazdidEMFDlI5KNJv6jbGdQAfPzLWzq_9Fl-wbmGu4Up6COLeQDdGybmPCv3a0id9-jVlyZRauUc2RtO3mJdQaOUZNOuDFe2WT0xmhPhoLXKEWhlF1udJGzm0beY7s0pwg4QjhGi7T75d3lQ\",\"e\":\"AQAB\",\"key_ops\":[\"verify\"],\"ext\":true},{\"kid\":\"-tvrpKoUfPU\",\"kty\":\"RSA\",\"alg\":\"RS512\",\"n\":\"sGoD0dZ3-F4NaAKQQrPhSGAbkBvJ0OsA3PQiwAhWXMrdOj0n9o2QDQfICdayIHcRwsisQaOz7B-L-pYcYFgMcdhj-yWOYVUMHXJCTqJeiz4zdGqjlCxkOF9ebauahRk_IoXp_hDyx3hqB98giajTn1gpW9G36Hcau4PutfM_jeyxi4iVeQSm8-p7qLCm-qD7ot0vzCqvsIt9cDvm9o5DVwG-LoAzeyZlEDmuDpFrLsFLz-D9rxLfSSdAethycrDA0Fhe1j7Az15NYUItJIkvyM5AsC6X0xyjO1LJWoJfbl9cbfaJQ0c3XKRc_NuxPYcDuM15gw0FaTkgUOYezuALPQ\",\"e\":\"AQAB\",\"key_ops\":[\"verify\"],\"ext\":true},{\"kid\":\"DA4MU9NJoDo\",\"kty\":\"RSA\",\"alg\":\"RS256\",\"n\":\"vaugR-F0yLkwgUyJPrIxjUrncPFoGlFyfPWYfVlU4OleYqNnEofoUNI2afIqRQlZgMgfxhknAi6IEhQKdNQGOAqzKoBxEvVK30x4XxMyVPMYYWbTCEEX2K5Kna0wJQ39rsajBWBRusxge_bLydt0dun4ZX4lv_BaCkdKn4TB6x3sQtwPyYrkOj7nykBiLzpcNGYKYbL5B5HGJHbcjXqX6ttvUn7B7E_idcyJbFfcwzrSj87Zdp5aj5LSiIqHtih8Sm2Mch5VQDT_U55UT4n8Rc9K_fJ1bQirXQdZZ9CgU4zcjXx8PnaEpeq18EcXxXsdfCN8o4EBUN7YWog1-65CLw\",\"e\":\"AQAB\",\"key_ops\":[\"verify\"],\"ext\":true},{\"kid\":\"re686ijoKyE\",\"kty\":\"RSA\",\"alg\":\"RS384\",\"n\":\"p3SLWJT-PyQn6n-fvj_s5dNqRJQauAZCC8JFnwUUP6n_LUfCG5tpfRLzZ0yXtsaPfN-z9c8JbXIbsF0HJN3NLTaBOWaZ04yjl3S29_8y-KwOMKlOafuRbYy7sXkicvCnjXRIcV85pAewrpwwHBcI2wDPDbkflwI3m6nurThQAQBDxPWz83QnC_iQ_V3pJGoGYItUGH_sJC06mqATV05dVzHSS0VZSFhEoyEtpykZNnhzgZCOACi4V8P07y0jsxQtqdPYYpqt9qg0Tjp81r-tIIDY7LEWv949rbR3Oad-QBcQC0f4CEapT2nbcVcadODtqPv1pQzSL7sSJ42zATGSxQ\",\"e\":\"AQAB\",\"key_ops\":[\"verify\"],\"ext\":true},{\"kid\":\"jGE5qoPDlnI\",\"kty\":\"RSA\",\"alg\":\"RS512\",\"n\":\"-WuAFX1DLbsMHagMw_NDJA6yxCcIm2SDgbMmDKOwCN4dR0YWdwKzDJ31EeoakDg-T85HaU48R4Kcx1WQUYnly98vDymMFl4Xe6zS9je-Uh3IVfkYJV3pwVKr2gj1hrX7QglIktLg0o0iWulFe1K1NQ5u7Jbe7ZZhOaetlTFH8KUgS78fajfhcFtrXBliqxHTiBz2lMYCj2ntqX8K8ue3_lVLBXm53AJG3T-ymnUd-nxJ-NsdFMSIzQO1-KUona9OgxDpu1cFoQ-YzOteSz2OP58wDl3gL5urIhzK7Hvu3I3okzlL2TyCHmOhLn6HMHr2-mwEgsYYnsEQwzu6bbY8hw\",\"e\":\"AQAB\",\"key_ops\":[\"verify\"],\"ext\":true},{\"kid\":\"PhN1R2mze2k\",\"kty\":\"RSA\",\"alg\":\"RS256\",\"n\":\"yGhTYJWPKU-n1gnPQPf1_Nr8HBJ6F-IT6PyxLJoURWrd_DvuW0EFQD1JolKAYWakuJAIWGtb8CXjRdzXrEVYNfshb-bBGYGotISxAD2z_KzEtcBya_TwywAQiMYildsAPRZR7HqvxnUw2iFUwec_gw6aBvu3rPcLnj6z2KKDJDFC1P4LHNDArh4EquhkgtzeD5h6-8cCI1iKpb-Op5nCe1DjUNg49L5NZhNtDlhCEb8P2LEbcV8vldIz9gJu0MSf94zzWu2JGsVtVQ29XPO8SQqrKQ5TKClmR1jIIGCF7t4AUoaaL6AA81F3cyEhKB3vN_D8DzEsr7tuwomLFZXmBw\",\"e\":\"AQAB\",\"key_ops\":[\"verify\"],\"ext\":true}]}" + } +} \ No newline at end of file diff --git a/test/resources/accounts-strict-origin-off/bob/shared-with-alice.txt b/test/resources/accounts-strict-origin-off/bob/shared-with-alice.txt new file mode 100644 index 000000000..304c0b4f3 --- /dev/null +++ b/test/resources/accounts-strict-origin-off/bob/shared-with-alice.txt @@ -0,0 +1 @@ +protected contents diff --git a/test/resources/accounts-strict-origin-off/bob/shared-with-alice.txt.acl b/test/resources/accounts-strict-origin-off/bob/shared-with-alice.txt.acl new file mode 100644 index 000000000..acdaa2eac --- /dev/null +++ b/test/resources/accounts-strict-origin-off/bob/shared-with-alice.txt.acl @@ -0,0 +1,15 @@ +<#Alice> + a ; + + <./shared-with-alice.txt>; + + # Alice web id + ; + + # Bob web id + ; + + + , + , + . diff --git a/test/resources/accounts/errortests/.acl b/test/resources/accounts/errortests/.acl-override similarity index 93% rename from test/resources/accounts/errortests/.acl rename to test/resources/accounts/errortests/.acl-override index f7be11c66..c04e83200 100644 --- a/test/resources/accounts/errortests/.acl +++ b/test/resources/accounts/errortests/.acl-override @@ -9,7 +9,7 @@ acl:accessTo ; # All resources will inherit this authorization, by default - acl:defaultForNew ; + acl:default ; # The owner has all of the access modes allowed acl:mode diff --git a/test/resources/accounts/localhost/.acl b/test/resources/accounts/localhost/.acl deleted file mode 100644 index ab393759f..000000000 --- a/test/resources/accounts/localhost/.acl +++ /dev/null @@ -1,10 +0,0 @@ -<#0> - a ; - <./> ; - ; - , , . - -<#0> - <./> ; - ; - , , . diff --git a/default-templates/server/index.html.acl b/test/resources/accounts/localhost/api/.acl similarity index 71% rename from default-templates/server/index.html.acl rename to test/resources/accounts/localhost/api/.acl index de9032975..40c1604a5 100644 --- a/default-templates/server/index.html.acl +++ b/test/resources/accounts/localhost/api/.acl @@ -1,11 +1,10 @@ +# Root ACL resource for the root @prefix acl: . @prefix foaf: . <#public> a acl:Authorization; - acl:agentClass foaf:Agent; # everyone - - acl:accessTo <./index.html>; - + acl:accessTo <./>; + acl:default <./>; acl:mode acl:Read. diff --git a/test/resources/accounts/localhost/samplePublicContainer/.acl b/test/resources/accounts/localhost/samplePublicContainer/.acl index 7942750c0..2539dce13 100644 --- a/test/resources/accounts/localhost/samplePublicContainer/.acl +++ b/test/resources/accounts/localhost/samplePublicContainer/.acl @@ -2,5 +2,5 @@ a ; <./>; ; - <./>; + <./>; , , . diff --git a/test/resources/accounts/localhost/sampleUser1Container/.acl b/test/resources/accounts/localhost/sampleUser1Container/.acl index 017dc7ad3..c2ad9a5d3 100644 --- a/test/resources/accounts/localhost/sampleUser1Container/.acl +++ b/test/resources/accounts/localhost/sampleUser1Container/.acl @@ -2,5 +2,5 @@ a ; <./>; ; - <./>; + <./>; , , . diff --git a/test/resources/accounts/tim.localhost/.acl b/test/resources/accounts/tim.localhost/.acl index ab393759f..187022ef2 100644 --- a/test/resources/accounts/tim.localhost/.acl +++ b/test/resources/accounts/tim.localhost/.acl @@ -1,6 +1,6 @@ <#0> a ; - <./> ; + <./> ; ; , , . diff --git a/test/resources/acl-tls/append-inherited/.acl b/test/resources/acl-tls/append-inherited/.acl index 9bfe07b39..725348121 100644 --- a/test/resources/acl-tls/append-inherited/.acl +++ b/test/resources/acl-tls/append-inherited/.acl @@ -9,5 +9,5 @@ acl:mode acl:Read, acl:Write, acl:Control; - acl:defaultForNew <./>. + acl:default <./>. diff --git a/test/resources/acl-tls/fake-account/.acl b/test/resources/acl-tls/fake-account/.acl index f49950774..2f2284163 100644 --- a/test/resources/acl-tls/fake-account/.acl +++ b/test/resources/acl-tls/fake-account/.acl @@ -1,5 +1,5 @@ <#0> a ; - <./> ; + <./> ; ; , . diff --git a/test/resources/acl-tls/origin/.acl b/test/resources/acl-tls/origin/.acl index 52aa6cc78..0e56dc986 100644 --- a/test/resources/acl-tls/origin/.acl +++ b/test/resources/acl-tls/origin/.acl @@ -1,5 +1,5 @@ <#0> a ; - <./> ; + <./> ; ; , . diff --git a/test/resources/acl-tls/owner-only/.acl b/test/resources/acl-tls/owner-only/.acl index 52aa6cc78..0e56dc986 100644 --- a/test/resources/acl-tls/owner-only/.acl +++ b/test/resources/acl-tls/owner-only/.acl @@ -1,5 +1,5 @@ <#0> a ; - <./> ; + <./> ; ; , . diff --git a/test/resources/acl-tls/read-acl/.acl b/test/resources/acl-tls/read-acl/.acl index c1ff5f067..b0f89fbb8 100644 --- a/test/resources/acl-tls/read-acl/.acl +++ b/test/resources/acl-tls/read-acl/.acl @@ -7,4 +7,4 @@ a ; <./>; ; - . \ No newline at end of file + . diff --git a/test/resources/acl-tls/write-acl/.acl b/test/resources/acl-tls/write-acl/.acl index 52aa6cc78..0e56dc986 100644 --- a/test/resources/acl-tls/write-acl/.acl +++ b/test/resources/acl-tls/write-acl/.acl @@ -1,5 +1,5 @@ <#0> a ; - <./> ; + <./> ; ; , . diff --git a/test/resources/acl-tls/write-acl/test-file$.ttl b/test/resources/acl-tls/write-acl/test-file$.ttl new file mode 100644 index 000000000..ce002f06a --- /dev/null +++ b/test/resources/acl-tls/write-acl/test-file$.ttl @@ -0,0 +1 @@ + . \ No newline at end of file diff --git a/test/resources/config/defaults.js b/test/resources/config/defaults.js new file mode 100644 index 000000000..9e5d92c50 --- /dev/null +++ b/test/resources/config/defaults.js @@ -0,0 +1,5 @@ +'use strict' + +module.exports = { + 'originsAllowed': ['https://test.apps.solid.invalid'] +} diff --git a/test/resources/headers/.acl b/test/resources/headers/.acl new file mode 100644 index 000000000..05a9842d9 --- /dev/null +++ b/test/resources/headers/.acl @@ -0,0 +1,10 @@ +# Root ACL resource for the root +@prefix acl: . +@prefix foaf: . + +<#public> + a acl:Authorization; + acl:agentClass foaf:Agent; # everyone + acl:accessTo ; + acl:default ; + acl:mode acl:Read. diff --git a/test/resources/invalid1.ttl b/test/resources/invalid1.ttl new file mode 100644 index 000000000..a2bc6473c --- /dev/null +++ b/test/resources/invalid1.ttl @@ -0,0 +1,13 @@ +@prefix ldp: . +@prefix o: . + + + test o:NetWorth; + o:netWorthOf ; + o:asset + , + ; + o:liability + , + , + . diff --git a/test/resources/invalid2.ttl b/test/resources/invalid2.ttl new file mode 100644 index 000000000..74afdb8bb --- /dev/null +++ b/test/resources/invalid2.ttl @@ -0,0 +1,9 @@ +@prefix txn: . +@prefix srv: . +@prefix log: . +@prefix xsd: . + +txn:123 invalid log:Transaction ; + log:processedBy srv:A ; + log:processedAt "2015-10-16T10:22:23"^^xsd:dateTime ; + log:statusCode 200 . diff --git a/test/resources/patch/.acl b/test/resources/patch/.acl index 8df5285c2..4f7a7fee7 100644 --- a/test/resources/patch/.acl +++ b/test/resources/patch/.acl @@ -2,6 +2,6 @@ <#Owner> a acl:Authorization; acl:accessTo ; - acl:defaultForNew ; + acl:default ; acl:agent ; acl:mode acl:Read, acl:Write, acl:Control. diff --git a/test/resources/sampleContainer/example4$.ttl b/test/resources/sampleContainer/example4$.ttl new file mode 100644 index 000000000..fd650c9a2 --- /dev/null +++ b/test/resources/sampleContainer/example4$.ttl @@ -0,0 +1,7 @@ +@prefix : . + +:a :b "The first line\nThe second line\n more" . + +:a :b """The first line +The second line + more""" . diff --git a/test/unit/account-manager-test.js b/test/unit/account-manager-test.js index 6d69bd391..3d0ac3188 100644 --- a/test/unit/account-manager-test.js +++ b/test/unit/account-manager-test.js @@ -17,6 +17,7 @@ const AccountManager = require('../../lib/models/account-manager') const UserAccount = require('../../lib/models/user-account') const TokenService = require('../../lib/services/token-service') const WebIdTlsCertificate = require('../../lib/models/webid-tls-certificate') +const ResourceMapper = require('../../lib/resource-mapper') const testAccountsDir = path.join(__dirname, '../resources/accounts') @@ -102,23 +103,33 @@ describe('AccountManager', () => { describe('accountDirFor()', () => { it('should match the solid root dir config, in single user mode', () => { let multiuser = false - let store = new LDP({ root: testAccountsDir, multiuser }) + let resourceMapper = new ResourceMapper({ + rootUrl: 'https://localhost:8443/', + includeHost: multiuser, + rootPath: testAccountsDir + }) + let store = new LDP({ multiuser, resourceMapper }) let options = { multiuser, store, host } let accountManager = AccountManager.from(options) let accountDir = accountManager.accountDirFor('alice') - expect(accountDir).to.equal(store.root) + expect(accountDir).to.equal(store.resourceMapper._rootPath) }) it('should compose the account dir in multi user mode', () => { let multiuser = true - let store = new LDP({ root: testAccountsDir, multiuser }) + let resourceMapper = new ResourceMapper({ + rootUrl: 'https://localhost:8443/', + includeHost: multiuser, + rootPath: testAccountsDir + }) + let store = new LDP({ multiuser, resourceMapper }) let host = SolidHost.from({ serverUri: 'https://localhost' }) let options = { multiuser, store, host } let accountManager = AccountManager.from(options) let accountDir = accountManager.accountDirFor('alice') - expect(accountDir).to.equal(path.join(testAccountsDir, 'alice.localhost')) + expect(accountDir).to.equal(testAccountsDir + '/alice.localhost') }) }) @@ -302,7 +313,12 @@ describe('AccountManager', () => { describe('rootAclFor()', () => { it('should return the server root .acl in single user mode', () => { - let store = new LDP({ suffixAcl: '.acl', multiuser: false }) + let resourceMapper = new ResourceMapper({ + rootUrl: 'https://localhost:8443/', + rootPath: process.cwd(), + includeHost: false + }) + let store = new LDP({ suffixAcl: '.acl', multiuser: false, resourceMapper }) let options = { host, multiuser: false, store } let accountManager = AccountManager.from(options) @@ -314,7 +330,12 @@ describe('AccountManager', () => { }) it('should return the profile root .acl in multi user mode', () => { - let store = new LDP({ suffixAcl: '.acl', multiuser: true }) + let resourceMapper = new ResourceMapper({ + rootUrl: 'https://localhost:8443/', + rootPath: process.cwd(), + includeHost: true + }) + let store = new LDP({ suffixAcl: '.acl', multiuser: true, resourceMapper }) let options = { host, multiuser: true, store } let accountManager = AccountManager.from(options) diff --git a/test/unit/acl-checker-test.js b/test/unit/acl-checker-test.js index 424c4b542..81571f802 100644 --- a/test/unit/acl-checker-test.js +++ b/test/unit/acl-checker-test.js @@ -7,28 +7,6 @@ chai.use(require('chai-as-promised')) const options = { fetch: (url, callback) => {} } describe('ACLChecker unit test', () => { - let acl - - beforeEach(() => { - acl = new ACLChecker('http://ex.com/.acl', options) - }) - - describe('checkAccess', () => { - it('should callback with null on grant success', () => { - let acls = { checkAccess: () => Promise.resolve(true) } - return expect(acl.checkAccess(acls)).to.eventually.be.true - }) - it('should callback with error on grant failure', () => { - let acls = { checkAccess: () => Promise.resolve(false) } - return expect(acl.checkAccess(acls)) - .to.be.rejectedWith('ACL file found but no matching policy found') - }) - it('should callback with error on grant error', () => { - let acls = { checkAccess: () => Promise.reject(new Error('my error')) } - return expect(acl.checkAccess(acls)).to.be.rejectedWith('my error') - }) - }) - describe('getPossibleACLs', () => { it('returns all possible ACLs of the root', () => { const aclChecker = new ACLChecker('http://ex.org/', options) diff --git a/test/unit/legacy-resource-mapper-test.js b/test/unit/legacy-resource-mapper-test.js deleted file mode 100644 index f9efec22c..000000000 --- a/test/unit/legacy-resource-mapper-test.js +++ /dev/null @@ -1,246 +0,0 @@ -const LegacyResourceMapper = require('../../lib/legacy-resource-mapper') -const chai = require('chai') -const { expect } = chai -chai.use(require('chai-as-promised')) - -const rootUrl = 'http://localhost/' -const rootPath = '/var/www/folder/' - -const itMapsUrl = asserter(mapsUrl) -const itMapsFile = asserter(mapsFile) - -describe('LegacyResourceMapper', () => { - describe('A LegacyResourceMapper instance for a single-host setup', () => { - const mapper = new LegacyResourceMapper({ rootUrl, rootPath }) - - // adapted PUT base cases from https://www.w3.org/DesignIssues/HTTPFilenameMapping.html - - itMapsUrl(mapper, 'a URL with an extension that matches the content type', - { - url: 'http://localhost/space/foo.html', - contentType: 'text/html', - createIfNotExists: true - }, - { - path: `${rootPath}space/foo.html`, - contentType: 'text/html' - }) - - // Additional PUT cases - - itMapsUrl(mapper, 'a URL without content type', - { - url: 'http://localhost/space/foo.html', - createIfNotExists: true - }, - { - path: `${rootPath}space/foo.html`, - contentType: 'text/html' - }) - - itMapsUrl(mapper, 'a URL with an alternative extension that matches the content type', - { - url: 'http://localhost/space/foo.jpeg', - contentType: 'image/jpeg', - createIfNotExists: true - }, - { - path: `${rootPath}space/foo.jpeg`, - contentType: 'image/jpeg' - }) - - itMapsUrl(mapper, 'a URL with an uppercase extension that matches the content type', - { - url: 'http://localhost/space/foo.JPG', - contentType: 'image/jpeg', - createIfNotExists: true - }, - { - path: `${rootPath}space/foo.JPG`, - contentType: 'image/jpeg' - }) - - itMapsUrl(mapper, 'a URL with a mixed-case extension that matches the content type', - { - url: 'http://localhost/space/foo.jPeG', - contentType: 'image/jpeg', - createIfNotExists: true - }, - { - path: `${rootPath}space/foo.jPeG`, - contentType: 'image/jpeg' - }) - - // GET/HEAD/POST/DELETE/PATCH base cases - - itMapsUrl(mapper, 'a URL of an existing file with extension', - { - url: 'http://localhost/space/foo.html' - }, - { - path: `${rootPath}space/foo.html`, - contentType: 'text/html' - }) - - itMapsUrl(mapper, 'an extensionless URL of an existing file', - { - url: 'http://localhost/space/foo' - }, - { - path: `${rootPath}space/foo`, - contentType: 'text/turtle' - }) - - itMapsUrl(mapper, 'a URL of an existing file with encoded characters', - { - url: 'http://localhost/space/foo%20bar%20bar.html' - }, - { - path: `${rootPath}space/foo bar bar.html`, - contentType: 'text/html' - }) - - itMapsUrl(mapper, 'a URL of a new file with encoded characters', - { - url: 'http://localhost/space%2Ffoo%20bar%20bar.html', - contentType: 'text/html', - createIfNotExists: true - }, - { - path: `${rootPath}space/foo bar bar.html`, - contentType: 'text/html' - }) - - // Security cases - - itMapsUrl(mapper, 'a URL with a /.. path segment', - { - url: 'http://localhost/space/../bar' - }, - new Error('Disallowed /.. segment in URL')) - - itMapsUrl(mapper, 'a URL with an encoded /.. path segment', - { - url: 'http://localhost/space%2F..%2Fbar' - }, - new Error('Disallowed /.. segment in URL')) - - // File to URL mapping - - itMapsFile(mapper, 'an HTML file', - { path: `${rootPath}space/foo.html` }, - { - url: 'http://localhost/space/foo.html', - contentType: 'text/html' - }) - - itMapsFile(mapper, 'a Turtle file', - { path: `${rootPath}space/foo.ttl` }, - { - url: 'http://localhost/space/foo.ttl', - contentType: 'text/turtle' - }) - - itMapsFile(mapper, 'a file with an uppercase extension', - { path: `${rootPath}space/foo.HTML` }, - { - url: 'http://localhost/space/foo.HTML', - contentType: 'text/html' - }) - - itMapsFile(mapper, 'a file with a mixed-case extension', - { path: `${rootPath}space/foo.HtMl` }, - { - url: 'http://localhost/space/foo.HtMl', - contentType: 'text/html' - }) - - itMapsFile(mapper, 'a file with disallowed IRI characters', - { path: `${rootPath}space/foo bar bar.html` }, - { - url: 'http://localhost/space/foo%20bar%20bar.html', - contentType: 'text/html' - }) - }) - - describe('A LegacyResourceMapper instance for a multi-host setup', () => { - const mapper = new LegacyResourceMapper({ rootUrl, rootPath, includeHost: true }) - - itMapsUrl(mapper, 'a URL with a host', - { - url: 'http://example.org/space/foo.html', - contentType: 'text/html', - createIfNotExists: true - }, - { - path: `${rootPath}example.org/space/foo.html`, - contentType: 'text/html' - }) - - itMapsUrl(mapper, 'a URL with a host with a port', - { - url: 'http://example.org:3000/space/foo.html', - contentType: 'text/html', - createIfNotExists: true - }, - { - path: `${rootPath}example.org/space/foo.html`, - contentType: 'text/html' - }) - - itMapsFile(mapper, 'a file on a host', - { - path: `${rootPath}space/foo.html`, - hostname: 'example.org' - }, - { - url: 'http://example.org/space/foo.html', - contentType: 'text/html' - }) - }) - - describe('A LegacyResourceMapper instance for a multi-host setup with a subfolder root URL', () => { - const rootUrl = 'http://localhost/foo/bar/' - const mapper = new LegacyResourceMapper({ rootUrl, rootPath, includeHost: true }) - - itMapsFile(mapper, 'a file on a host', - { - path: `${rootPath}space/foo.html`, - hostname: 'example.org' - }, - { - url: 'http://example.org/foo/bar/space/foo.html', - contentType: 'text/html' - }) - }) -}) - -function asserter (assert) { - const f = (...args) => assert(it, ...args) - f.skip = (...args) => assert(it.skip, ...args) - f.only = (...args) => assert(it.only, ...args) - return f -} - -function mapsUrl (it, mapper, label, options, expected) { - // Set up positive test - if (!(expected instanceof Error)) { - it(`maps ${label}`, async () => { - const actual = await mapper.mapUrlToFile(options) - expect(actual).to.deep.equal(expected) - }) - // Set up error test - } else { - it(`does not map ${label}`, async () => { - const actual = mapper.mapUrlToFile(options) - await expect(actual).to.be.rejectedWith(expected.message) - }) - } -} - -function mapsFile (it, mapper, label, options, expected) { - it(`maps ${label}`, async () => { - const actual = await mapper.mapFileToUrl(options) - expect(actual).to.deep.equal(expected) - }) -} diff --git a/test/unit/options.js b/test/unit/options.js new file mode 100644 index 000000000..641dee55f --- /dev/null +++ b/test/unit/options.js @@ -0,0 +1,18 @@ +var assert = require('chai').assert + +var options = require('../../bin/lib/options') + +describe('Command line options', function () { + describe('options', function () { + it('is an array', function () { + assert.equal(Array.isArray(options), true) + }) + + it('contains only `name`s that are kebab-case', function () { + assert.equal( + options.every(({name}) => (/^[a-z][a-z0-9-]*$/).test(name)), + true + ) + }) + }) +}) diff --git a/test/unit/resource-mapper-test.js b/test/unit/resource-mapper-test.js index 9575a9541..bc669fdc7 100644 --- a/test/unit/resource-mapper-test.js +++ b/test/unit/resource-mapper-test.js @@ -11,7 +11,11 @@ const itMapsFile = asserter(mapsFile) describe('ResourceMapper', () => { describe('A ResourceMapper instance for a single-host setup', () => { - const mapper = new ResourceMapper({ rootUrl, rootPath }) + const mapper = new ResourceMapper({ + rootUrl, + rootPath, + includeHost: false + }) // PUT base cases from https://www.w3.org/DesignIssues/HTTPFilenameMapping.html @@ -93,6 +97,28 @@ describe('ResourceMapper', () => { contentType: 'image/jpeg' }) + itMapsUrl(mapper, 'a URL with an overridden extension that matches the content type', + { + url: 'http://localhost/space/foo.acl', + contentType: 'text/turtle', + createIfNotExists: true + }, + { + path: `${rootPath}space/foo.acl`, + contentType: 'text/turtle' + }) + + itMapsUrl(mapper, 'a URL with an alternative overridden extension that matches the content type', + { + url: 'http://localhost/space/foo.acl', + contentType: 'text/n3', + createIfNotExists: true + }, + { + path: `${rootPath}space/foo.acl$.n3`, + contentType: 'text/n3' + }) + // GET/HEAD/POST/DELETE/PATCH base cases itMapsUrl(mapper, 'a URL of a non-existing file', @@ -187,6 +213,160 @@ describe('ResourceMapper', () => { contentType: 'text/html' }) + itMapsUrl(mapper, 'a URL of an existing .acl file', + { + url: 'http://localhost/space/.acl' + }, + [ + `${rootPath}space/.acl` + ], + { + path: `${rootPath}space/.acl`, + contentType: 'text/turtle' + }) + + itMapsUrl(mapper, 'a URL of an existing .acl file with a different content type', + { + url: 'http://localhost/space/.acl' + }, + [ + `${rootPath}space/.acl$.n3` + ], + { + path: `${rootPath}space/.acl$.n3`, + contentType: 'text/n3' + }) + + itMapsUrl(mapper, 'a URL ending with a slash when index.html is available', + { + url: 'http://localhost/space/' + }, + [ + `${rootPath}space/index.html`, + `${rootPath}space/index$.ttl` + ], + { + path: `${rootPath}space/index.html`, + contentType: 'text/html' + }) + + itMapsUrl(mapper, 'a URL ending with a slash when index.ttl is available', + { + url: 'http://localhost/space/' + }, + [ + `${rootPath}space/index.ttl` + ], + { + path: `${rootPath}space/`, + contentType: 'application/octet-stream' + }) + + itMapsUrl(mapper, 'a URL ending with a slash when index$.html is available', + { + url: 'http://localhost/space/' + }, + [ + `${rootPath}space/index$.html`, + `${rootPath}space/index$.ttl` + ], + { + path: `${rootPath}space/`, + contentType: 'application/octet-stream' + }) + + itMapsUrl(mapper, 'a URL ending with a slash when index$.ttl is available', + { + url: 'http://localhost/space/' + }, + [ + `${rootPath}space/index$.ttl` + ], + { + path: `${rootPath}space/`, + contentType: 'application/octet-stream' + }) + + itMapsUrl(mapper, 'a URL ending with a slash to a folder when index.html is available but index is skipped', + { + url: 'http://localhost/space/', + searchIndex: false + }, + [ + `${rootPath}space/index.html`, + `${rootPath}space/index$.ttl` + ], + { + path: `${rootPath}space/`, + contentType: 'application/octet-stream' + }) + + itMapsUrl(mapper, 'a URL ending with a slash to a folder when no index is available', + { + url: 'http://localhost/space/' + }, + { + path: `${rootPath}space/`, + contentType: 'application/octet-stream' + }) + + itMapsUrl(mapper, 'a URL of that has an accompanying acl file, but no actual file', + { + url: 'http://localhost/space/' + }, + [ + `${rootPath}space/index.acl` + ], + { + path: `${rootPath}space/`, + contentType: 'application/octet-stream' + }) + + itMapsUrl(mapper, 'a URL ending with a slash for text/html when index.html is not available', + { + url: 'http://localhost/space/', + contentType: 'text/html', + createIfNotExists: true + }, + { + path: `${rootPath}space/index.html`, + contentType: 'text/html' + }) + + itMapsUrl(mapper, 'a URL of that has an accompanying meta file, but no actual file', + { + url: 'http://localhost/space/', + contentType: 'text/html', + createIfNotExists: true + }, + [ + `${rootPath}space/index.meta` + ], + { + path: `${rootPath}space/index.html`, + contentType: 'text/html' + }) + + itMapsUrl(mapper, 'a URL ending with a slash to a folder when index is skipped', + { + url: 'http://localhost/space/', + contentType: 'application/octet-stream', + createIfNotExists: true, + searchIndex: false + }, + { + path: `${rootPath}space/`, + contentType: 'application/octet-stream' + }) + + itMapsUrl(mapper, 'a URL ending with a slash for text/turtle', + { + url: 'http://localhost/space/', + contentType: 'text/turtle', + createIfNotExists: true + }, + new Error('Index file needs to have text/html as content type')) + // Security cases itMapsUrl(mapper, 'a URL with an unknown content type', @@ -228,6 +408,13 @@ describe('ResourceMapper', () => { contentType: 'text/turtle' }) + itMapsFile(mapper, 'an ACL file', + { path: `${rootPath}space/.acl` }, + { + url: 'http://localhost/space/.acl', + contentType: 'text/turtle' + }) + itMapsFile(mapper, 'an unknown file type', { path: `${rootPath}space/foo.bar` }, { @@ -306,6 +493,34 @@ describe('ResourceMapper', () => { contentType: 'text/html' }) + itMapsUrl(mapper, 'a URL with a host specified as a URL object', + { + url: { + hostname: 'example.org', + path: '/space/foo.html' + }, + contentType: 'text/html', + createIfNotExists: true + }, + { + path: `${rootPath}example.org/space/foo.html`, + contentType: 'text/html' + }) + + itMapsUrl(mapper, 'a URL with a host specified as an Express request object', + { + url: { + hostname: 'example.org', + pathname: '/space/foo.html' + }, + contentType: 'text/html', + createIfNotExists: true + }, + { + path: `${rootPath}example.org/space/foo.html`, + contentType: 'text/html' + }) + itMapsUrl(mapper, 'a URL with a host with a port', { url: 'http://example.org:3000/space/foo.html', @@ -415,7 +630,7 @@ function mapsUrl (it, mapper, label, options, files, expected) { // Mock filesystem function mockReaddir () { mapper._readdir = async (path) => { - expect(path).to.equal(`${rootPath}space/`) + expect(path.startsWith(`${rootPath}space/`)).to.equal(true) return files.map(f => f.replace(/.*\//, '')) } } diff --git a/test/unit/solid-host-test.js b/test/unit/solid-host-test.js index 19b7e95d7..fc2d77b6d 100644 --- a/test/unit/solid-host-test.js +++ b/test/unit/solid-host-test.js @@ -7,16 +7,24 @@ const defaults = require('../../config/defaults') describe('SolidHost', () => { describe('from()', () => { - it('should init with port, serverUri and hostname', () => { + it('should init with provided params', () => { let config = { port: 3000, - serverUri: 'https://localhost:3000' + serverUri: 'https://localhost:3000', + live: true, + root: '/data/solid/', + multiuser: true, + webid: true } let host = SolidHost.from(config) expect(host.port).to.equal(3000) expect(host.serverUri).to.equal('https://localhost:3000') expect(host.hostname).to.equal('localhost') + expect(host.live).to.be.true + expect(host.root).to.equal('/data/solid/') + expect(host.multiuser).to.be.true + expect(host.webid).to.be.true }) it('should init to default port and serverUri values', () => { @@ -51,27 +59,31 @@ describe('SolidHost', () => { }) it('should allow an empty userId and origin', () => { - expect(host.allowsSessionFor('', '')).to.be.true + expect(host.allowsSessionFor('', '', [])).to.be.true }) it('should allow a userId with empty origin', () => { - expect(host.allowsSessionFor('https://user.own/profile/card#me', '')).to.be.true + expect(host.allowsSessionFor('https://user.own/profile/card#me', '', [])).to.be.true }) it('should allow a userId with the user subdomain as origin', () => { - expect(host.allowsSessionFor('https://user.own/profile/card#me', 'https://user.own')).to.be.true + expect(host.allowsSessionFor('https://user.own/profile/card#me', 'https://user.own', [])).to.be.true }) it('should allow a userId with the server domain as origin', () => { - expect(host.allowsSessionFor('https://user.own/profile/card#me', 'https://test.local')).to.be.true + expect(host.allowsSessionFor('https://user.own/profile/card#me', 'https://test.local', [])).to.be.true }) it('should allow a userId with a server subdomain as origin', () => { - expect(host.allowsSessionFor('https://user.own/profile/card#me', 'https://other.test.local')).to.be.true + expect(host.allowsSessionFor('https://user.own/profile/card#me', 'https://other.test.local', [])).to.be.true }) it('should disallow a userId from a different domain', () => { - expect(host.allowsSessionFor('https://user.own/profile/card#me', 'https://other.remote')).to.be.false + expect(host.allowsSessionFor('https://user.own/profile/card#me', 'https://other.remote', [])).to.be.false + }) + + it('should allow user from a trusted domain', () => { + expect(host.allowsSessionFor('https://user.own/profile/card#me', 'https://other.remote', ['https://other.remote'])).to.be.true }) }) diff --git a/test/unit/user-accounts-api-test.js b/test/unit/user-accounts-api-test.js index f420dd002..32c909ed1 100644 --- a/test/unit/user-accounts-api-test.js +++ b/test/unit/user-accounts-api-test.js @@ -13,6 +13,7 @@ const LDP = require('../../lib/ldp') const SolidHost = require('../../lib/models/solid-host') const AccountManager = require('../../lib/models/account-manager') const testAccountsDir = path.join(__dirname, '..', 'resources', 'accounts') +var ResourceMapper = require('../../lib/resource-mapper') const api = require('../../lib/api/accounts/user-accounts') @@ -26,7 +27,12 @@ describe('api/accounts/user-accounts', () => { describe('newCertificate()', () => { describe('in multi user mode', () => { let multiuser = true - let store = new LDP({ root: testAccountsDir, multiuser }) + let resourceMapper = new ResourceMapper({ + rootUrl: 'https://localhost:8443/', + includeHost: multiuser, + rootPath: testAccountsDir + }) + let store = new LDP({ multiuser, resourceMapper }) it('should throw a 400 error if spkac param is missing', done => { let options = { host, store, multiuser, authMethod: 'oidc' } diff --git a/test/unit/user-utils-test.js b/test/unit/user-utils-test.js index 9170e7eac..67dea178d 100644 --- a/test/unit/user-utils-test.js +++ b/test/unit/user-utils-test.js @@ -1,8 +1,41 @@ const chai = require('chai') const expect = chai.expect const userUtils = require('../../lib/common/user-utils') +const $rdf = require('rdflib') describe('user-utils', () => { + describe('getName', () => { + let ldp + const webId = 'http://test#me' + const name = 'NAME' + + beforeEach(() => { + const store = $rdf.graph() + store.add($rdf.sym(webId), $rdf.sym('http://www.w3.org/2006/vcard/ns#fn'), $rdf.lit(name)) + ldp = { fetchGraph: () => Promise.resolve(store) } + }) + + it('should return name from graph', async () => { + const returnedName = await userUtils.getName(webId, ldp.fetchGraph) + expect(returnedName).to.equal(name) + }) + }) + + describe('getWebId', () => { + let fetchGraph + const webId = 'https://test.localhost:8443/profile/card#me' + const suffixMeta = '.meta' + + beforeEach(() => { + fetchGraph = () => Promise.resolve(`<${webId}> .`) + }) + + it('should return webId from meta file', async () => { + const returnedWebId = await userUtils.getWebId('foo', 'https://bar/', suffixMeta, fetchGraph) + expect(returnedWebId).to.equal(webId) + }) + }) + describe('isValidUsername', () => { it('should accect valid usernames', () => { const usernames = [ diff --git a/test/unit/utils-test.js b/test/unit/utils-test.js index 163714c91..21129296a 100644 --- a/test/unit/utils-test.js +++ b/test/unit/utils-test.js @@ -1,7 +1,6 @@ var assert = require('chai').assert var utils = require('../../lib/utils') -var path = require('path') describe('Utility functions', function () { describe('pathBasename', function () { @@ -22,18 +21,6 @@ describe('Utility functions', function () { }) }) - describe('uriToFilename', function () { - it('should decode hex-encoded space', function () { - assert.equal(utils.uriToFilename('uri%20', 'base/'), path.join('base', 'uri ')) - }) - it('should decode hex-encoded at sign', function () { - assert.equal(utils.uriToFilename('film%4011', 'base/'), path.join('base', 'film@11')) - }) - it('should decode hex-encoded single quote', function () { - assert.equal(utils.uriToFilename('quote%27', 'base/'), path.join('base', 'quote\'')) - }) - }) - describe('stripLineEndings()', () => { it('should pass through falsy string arguments', () => { assert.equal(utils.stripLineEndings(''), '') @@ -83,4 +70,37 @@ describe('Utility functions', function () { assert.equal(utils.fullUrlForReq(req), 'https://example.com/resource1?sort=desc') }) }) + + describe('getContentType()', () => { + describe('for Express headers', () => { + it('should default to text/plain', () => { + assert.equal(utils.getContentType({}), 'text/plain') + }) + + it('should get a basic content type', () => { + assert.equal(utils.getContentType({'content-type': 'text/html'}), 'text/html') + }) + + it('should get a content type without its charset', () => { + assert.equal(utils.getContentType({'content-type': 'text/html; charset=us-ascii'}), 'text/html') + }) + }) + + describe('for Fetch API headers', () => { + it('should default to text/plain', () => { + // eslint-disable-next-line no-undef + assert.equal(utils.getContentType(new Headers({})), 'text/plain') + }) + + it('should get a basic content type', () => { + // eslint-disable-next-line no-undef + assert.equal(utils.getContentType(new Headers({'content-type': 'text/html'})), 'text/html') + }) + + it('should get a content type without its charset', () => { + // eslint-disable-next-line no-undef + assert.equal(utils.getContentType(new Headers({'content-type': 'text/html; charset=us-ascii'})), 'text/html') + }) + }) + }) }) diff --git a/test/utils.js b/test/utils.js index ae181f8cf..59a31956a 100644 --- a/test/utils.js +++ b/test/utils.js @@ -1,8 +1,10 @@ -var fs = require('fs-extra') -var rimraf = require('rimraf') -var path = require('path') +const fs = require('fs-extra') +const rimraf = require('rimraf') +const path = require('path') const OIDCProvider = require('@solid/oidc-op') const dns = require('dns') +const ldnode = require('../index') +const supertest = require('supertest') const TEST_HOSTS = ['nic.localhost', 'tim.localhost', 'nicola.localhost'] @@ -12,6 +14,7 @@ exports.rm = function (file) { exports.cleanDir = function (dirPath) { fs.removeSync(path.join(dirPath, '.well-known/.acl')) + fs.removeSync(path.join(dirPath, '.acl')) fs.removeSync(path.join(dirPath, 'favicon.ico')) fs.removeSync(path.join(dirPath, 'favicon.ico.acl')) fs.removeSync(path.join(dirPath, 'index.html')) @@ -80,3 +83,14 @@ exports.loadProvider = function loadProvider (configPath) { return provider.initializeKeyChain(config.keys) }) } + +exports.createServer = createServer +function createServer (options) { + return ldnode.createServer(options) +} + +exports.setupSupertestServer = setupSuperServer +function setupSuperServer (options) { + const ldpServer = createServer(options) + return supertest(ldpServer) +}