From fd43a24a36bc0009465f6560d647fcfb049e6f98 Mon Sep 17 00:00:00 2001 From: Arne Hassel Date: Tue, 20 Nov 2018 18:06:43 +0100 Subject: [PATCH 01/12] Some simple bootstrapping --- common/js/auth-buttons.js | 8 ++++++++ lib/api/accounts/user-accounts.js | 3 +++ lib/requests/get-account-info-request.js | 25 ++++++++++++++++++++++++ 3 files changed, 36 insertions(+) create mode 100644 lib/requests/get-account-info-request.js diff --git a/common/js/auth-buttons.js b/common/js/auth-buttons.js index 49c28840e..8df0c51c2 100644 --- a/common/js/auth-buttons.js +++ b/common/js/auth-buttons.js @@ -1,4 +1,5 @@ /* Provide functionality for authentication buttons */ +/* global URL, location, alert, solid */ (({ auth }) => { // Wire up DOM elements @@ -8,6 +9,7 @@ loginButton.addEventListener('click', login) logoutButton.addEventListener('click', logout) registerButton.addEventListener('click', register) + document.addEventListener('DOMContentLoaded', getAccountInfo) // Track authentication status and update UI auth.trackSession(session => { @@ -46,4 +48,10 @@ registration.searchParams.set('returnToUrl', location) location.href = registration } + + async function getAccountInfo () { + const account = new URL('/account', location) + const response = await auth.fetch(account) + console.log('test3', response) + } })(solid) diff --git a/lib/api/accounts/user-accounts.js b/lib/api/accounts/user-accounts.js index cc5daafd0..e2ed163bd 100644 --- a/lib/api/accounts/user-accounts.js +++ b/lib/api/accounts/user-accounts.js @@ -8,6 +8,7 @@ const CreateAccountRequest = require('../../requests/create-account-request') const AddCertificateRequest = require('../../requests/add-cert-request') const DeleteAccountRequest = require('../../requests/delete-account-request') const DeleteAccountConfirmRequest = require('../../requests/delete-account-confirm-request') +const GetAccountInfoRequest = require('../../requests/get-account-info-request') /** * Returns an Express middleware handler for checking if a particular account @@ -70,6 +71,8 @@ function middleware (accountManager) { router.post('/api/accounts/cert', bodyParser, newCertificate(accountManager)) + router.get('/account', GetAccountInfoRequest.get) + router.get('/account/delete', DeleteAccountRequest.get) router.post('/account/delete', bodyParser, DeleteAccountRequest.post) diff --git a/lib/requests/get-account-info-request.js b/lib/requests/get-account-info-request.js new file mode 100644 index 000000000..6a66af2e6 --- /dev/null +++ b/lib/requests/get-account-info-request.js @@ -0,0 +1,25 @@ +'use strict' + +const AuthRequest = require('./auth-request') +// const debug = require('./../debug').accounts + +// class DeleteAccountRequest { +class GetAccountInfoRequest extends AuthRequest { + static get (req, res) { + const request = GetAccountInfoRequest.fromParams(req, res) + + console.log('GET account info', request) + } + + static fromParams (req, res) { + let locals = req.app.locals + let accountManager = locals.accountManager + + return new GetAccountInfoRequest({ + accountManager, + response: res + }) + } +} + +module.exports = GetAccountInfoRequest From 671031e2dfd8ef0fd7d37ed2294bfc31b9bde2db Mon Sep 17 00:00:00 2001 From: Arne Hassel Date: Tue, 20 Nov 2018 19:54:17 +0100 Subject: [PATCH 02/12] A simple API for returning info about account No authentication and caching yet --- lib/common/uri-utils.js | 5 ++++ lib/requests/get-account-info-request.js | 38 +++++++++++++++--------- lib/utils.js | 5 ++-- 3 files changed, 32 insertions(+), 16 deletions(-) create mode 100644 lib/common/uri-utils.js diff --git a/lib/common/uri-utils.js b/lib/common/uri-utils.js new file mode 100644 index 000000000..3a448f72c --- /dev/null +++ b/lib/common/uri-utils.js @@ -0,0 +1,5 @@ +module.exports.getFullUriFromRequest = getFullUriFromRequest + +function getFullUriFromRequest (req) { + return req.protocol + '://' + req.get('host') + req.originalUrl +} diff --git a/lib/requests/get-account-info-request.js b/lib/requests/get-account-info-request.js index 6a66af2e6..28f1e2b89 100644 --- a/lib/requests/get-account-info-request.js +++ b/lib/requests/get-account-info-request.js @@ -1,25 +1,35 @@ 'use strict' const AuthRequest = require('./auth-request') -// const debug = require('./../debug').accounts +const { actualSize } = require('../utils') +const { getFullUriFromRequest } = require('../common/uri-utils') +const $rdf = require('rdflib') -// class DeleteAccountRequest { -class GetAccountInfoRequest extends AuthRequest { - static get (req, res) { - const request = GetAccountInfoRequest.fromParams(req, res) +const SOLID = $rdf.Namespace('http://www.w3.org/ns/solid/terms#') - console.log('GET account info', request) +class GetAccountInfoRequest extends AuthRequest { + static async get (req, res) { + const contentType = 'application/ld+json' + const body = getBody(req, contentType) + res + .status(200) + .set('Content-Type', contentType) + .send(body) } +} - static fromParams (req, res) { - let locals = req.app.locals - let accountManager = locals.accountManager - - return new GetAccountInfoRequest({ - accountManager, - response: res +async function getBody (req, contentType) { + const root = req.app.locals.ldp.getRoot(req.hostname) + const size = await actualSize(root) + const store = $rdf.graph() + const baseUri = getFullUriFromRequest(req) + store.add($rdf.sym(baseUri), SOLID('storageUsage'), $rdf.lit(size)) + return await new Promise((resolve, reject) => { + $rdf.serialize(null, store, baseUri, contentType, (err, result) => { + if (err) return reject(err) + resolve(result) }) - } + }) } module.exports = GetAccountInfoRequest diff --git a/lib/utils.js b/lib/utils.js index 21e5f6870..a02911e7f 100644 --- a/lib/utils.js +++ b/lib/utils.js @@ -11,6 +11,7 @@ module.exports.getQuota = getQuota module.exports.overQuota = overQuota module.exports.getContentType = getContentType module.exports.parse = parse +module.exports.actualSize = actualSize const fs = require('fs') const path = require('path') @@ -233,11 +234,11 @@ async function overQuota (root, serverUri) { * so it needs to be rewritten. */ -function actualSize (root) { +async function actualSize (root) { return util.promisify(getSize)(root) } -function _asyncReadfile (filename) { +async function _asyncReadfile (filename) { return util.promisify(fs.readFile)(filename, 'utf-8') } From 91aa445cbf0fd0223783d42c2891960d23c62492 Mon Sep 17 00:00:00 2001 From: Arne Hassel Date: Fri, 18 Jan 2019 17:55:08 +0100 Subject: [PATCH 03/12] Added logic to ensure unauthenticated don't get access --- lib/acl-checker.js | 45 ++++++++++++++++++++++ lib/handlers/allow.js | 48 +----------------------- lib/ldp.js | 8 ++++ lib/requests/get-account-info-request.js | 25 +++++++++--- 4 files changed, 73 insertions(+), 53 deletions(-) diff --git a/lib/acl-checker.js b/lib/acl-checker.js index ece53a64d..ba22895b8 100644 --- a/lib/acl-checker.js +++ b/lib/acl-checker.js @@ -5,6 +5,8 @@ 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#') @@ -144,6 +146,49 @@ class ACLChecker { isAcl (resource) { return resource.endsWith(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 + }) + } +} + +/** + * 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 + } } // Returns the index of the last slash before the given position diff --git a/lib/handlers/allow.js b/lib/handlers/allow.js index 756f48554..80bbd4811 100644 --- a/lib/handlers/allow.js +++ b/lib/handlers/allow.js @@ -1,12 +1,8 @@ module.exports = allow -const $rdf = require('rdflib') const path = require('path') const ACL = require('../acl-checker') const debug = require('../debug.js').ACL -const fs = require('fs') -const { promisify } = require('util') -const HTTPError = require('../http-error') function allow (mode, checkPermissionsForDirectory) { return async function allowHandler (req, res, next) { @@ -43,25 +39,8 @@ function allow (mode, checkPermissionsForDirectory) { resourcePath += '/' } - 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 - req.acl = new ACL(rootUrl + resourcePath, { - 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: trustedOrigins - }) + req.acl = ACL.createFromLDPAndRequest(rootUrl + resourcePath, ldp, req) // Ensure the user has the required permission const userId = req.session.userId @@ -74,28 +53,3 @@ function allow (mode, checkPermissionsForDirectory) { next(error) } } - -/** - * 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/ldp.js b/lib/ldp.js index bcc7a5ee3..006bc2cc2 100644 --- a/lib/ldp.js +++ b/lib/ldp.js @@ -463,6 +463,14 @@ class LDP { return ensureNotExists(this, path.join(containerURI, filename)) } + 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) } diff --git a/lib/requests/get-account-info-request.js b/lib/requests/get-account-info-request.js index 28f1e2b89..1e5a82dff 100644 --- a/lib/requests/get-account-info-request.js +++ b/lib/requests/get-account-info-request.js @@ -1,25 +1,38 @@ 'use strict' +const ACLChecker = require('../acl-checker') const AuthRequest = require('./auth-request') const { actualSize } = require('../utils') const { getFullUriFromRequest } = require('../common/uri-utils') const $rdf = require('rdflib') +const HTTPError = require('../http-error') const SOLID = $rdf.Namespace('http://www.w3.org/ns/solid/terms#') class GetAccountInfoRequest extends AuthRequest { static async get (req, res) { const contentType = 'application/ld+json' - const body = getBody(req, contentType) - res - .status(200) - .set('Content-Type', contentType) - .send(body) + const body = await getBody(req, contentType) + if (body.status && body.status >= 400) { + return res.status(body.status).send(body.message) + } + return res.status(200).set('Content-Type', contentType).send(body) } } async function getBody (req, contentType) { - const root = req.app.locals.ldp.getRoot(req.hostname) + const ldp = req.app.locals.ldp + const rootUrl = `${ldp.resourceMapper.resolveUrl(req.hostname)}/` + const userId = req.session.userId + if (!userId) { + return new HTTPError(401, 'Requires authentication') + } + const acl = ACLChecker.createFromLDPAndRequest(rootUrl, ldp, req) + const isAllowed = await acl.can(userId, 'Control') + if (!isAllowed) { + return await req.acl.getError(userId, 'Control') + } + const root = ldp.resourceMapper._getFullPath(rootUrl) const size = await actualSize(root) const store = $rdf.graph() const baseUri = getFullUriFromRequest(req) From 0f0fa9e1b322da257bec49c5a0f6041e4d55a094 Mon Sep 17 00:00:00 2001 From: Arne Hassel Date: Mon, 21 Jan 2019 13:42:33 +0100 Subject: [PATCH 04/12] Getting data about quota in storage --- lib/common/uri-utils.js | 5 ---- lib/requests/get-account-info-request.js | 29 +++++++++++++++++++----- 2 files changed, 23 insertions(+), 11 deletions(-) delete mode 100644 lib/common/uri-utils.js diff --git a/lib/common/uri-utils.js b/lib/common/uri-utils.js deleted file mode 100644 index 3a448f72c..000000000 --- a/lib/common/uri-utils.js +++ /dev/null @@ -1,5 +0,0 @@ -module.exports.getFullUriFromRequest = getFullUriFromRequest - -function getFullUriFromRequest (req) { - return req.protocol + '://' + req.get('host') + req.originalUrl -} diff --git a/lib/requests/get-account-info-request.js b/lib/requests/get-account-info-request.js index 1e5a82dff..d817e12a4 100644 --- a/lib/requests/get-account-info-request.js +++ b/lib/requests/get-account-info-request.js @@ -3,15 +3,15 @@ const ACLChecker = require('../acl-checker') const AuthRequest = require('./auth-request') const { actualSize } = require('../utils') -const { getFullUriFromRequest } = require('../common/uri-utils') const $rdf = require('rdflib') const HTTPError = require('../http-error') +const PIM = $rdf.Namespace('http://www.w3.org/ns/pim/space#') const SOLID = $rdf.Namespace('http://www.w3.org/ns/solid/terms#') class GetAccountInfoRequest extends AuthRequest { static async get (req, res) { - const contentType = 'application/ld+json' + const contentType = req.get('content-type') || 'text/turtle' const body = await getBody(req, contentType) if (body.status && body.status >= 400) { return res.status(body.status).send(body.message) @@ -32,13 +32,30 @@ async function getBody (req, contentType) { if (!isAllowed) { return await req.acl.getError(userId, 'Control') } - const root = ldp.resourceMapper._getFullPath(rootUrl) + + // Get total available size of user's POD + const userIdGraph = await ldp.getGraph(userId) + const storageUrlNode = userIdGraph.any($rdf.sym(userId), PIM('storage'), null) + if (!storageUrlNode) { + return new HTTPError(404, 'Unable to find pim:storage for webId') + } + const serverSideUrl = `${storageUrlNode.value}settings/serverSide.ttl` // needs to be hardcoded for now + const serverSideGraph = await ldp.getGraph(serverSideUrl) + const quotaNode = serverSideGraph.any(storageUrlNode, SOLID('storageQuota'), null) + if (!quotaNode) { + return new HTTPError(404, 'Unable to find solid:storageQuota') + } + + // Get size of user's POD + const root = ldp.resourceMapper.getFullPath(rootUrl) const size = await actualSize(root) + + // Add triples to graph const store = $rdf.graph() - const baseUri = getFullUriFromRequest(req) - store.add($rdf.sym(baseUri), SOLID('storageUsage'), $rdf.lit(size)) + store.add(storageUrlNode, SOLID('storageUsage'), $rdf.lit(size)) + store.add(storageUrlNode, SOLID('storageQuota'), quotaNode.value) return await new Promise((resolve, reject) => { - $rdf.serialize(null, store, baseUri, contentType, (err, result) => { + $rdf.serialize(null, store, storageUrlNode.value, contentType, (err, result) => { if (err) return reject(err) resolve(result) }) From 43531251abb83a8ad0c63b8936d861ddc4dd45af Mon Sep 17 00:00:00 2001 From: Arne Hassel Date: Tue, 22 Jan 2019 14:09:54 +0100 Subject: [PATCH 05/12] A few changes to show quota and used storage on frontpage --- common/js/auth-buttons.js | 36 ++++++++++++++++++++---- default-templates/new-account/index.html | 14 +++++++++ 2 files changed, 44 insertions(+), 6 deletions(-) diff --git a/common/js/auth-buttons.js b/common/js/auth-buttons.js index 8df0c51c2..137d086e8 100644 --- a/common/js/auth-buttons.js +++ b/common/js/auth-buttons.js @@ -1,7 +1,7 @@ /* Provide functionality for authentication buttons */ /* global URL, location, alert, solid */ -(({ auth }) => { +(({ auth }, $rdf) => { // Wire up DOM elements const [loginButton, logoutButton, registerButton, accountSettings] = ['login', 'logout', 'register', 'account-settings'].map(id => @@ -9,7 +9,9 @@ loginButton.addEventListener('click', login) logoutButton.addEventListener('click', logout) registerButton.addEventListener('click', register) - document.addEventListener('DOMContentLoaded', getAccountInfo) + if ($rdf) { + document.addEventListener('DOMContentLoaded', getAccountInfo) + } // Track authentication status and update UI auth.trackSession(session => { @@ -50,8 +52,30 @@ } async function getAccountInfo () { - const account = new URL('/account', location) - const response = await auth.fetch(account) - console.log('test3', response) + const SOLID = $rdf.Namespace('http://www.w3.org/ns/solid/terms#') + + // load data + const accountUrl = new URL('/account', location).href + const accountStore = $rdf.graph() + const accountFetcher = new $rdf.Fetcher(accountStore) + await accountFetcher.load(accountUrl) + + // get DOM and data-dependencies + const quotaNode = accountStore.any($rdf.sym(accountUrl), SOLID('storageQuota')) + const usageNode = accountStore.any($rdf.sym(accountUrl), SOLID('storageUsage')) + const storagePanel = document.getElementById('StoragePanel') + const storageQuota = document.getElementById('StorageQuota') + const storageUsage = document.getElementById('StorageUsage') + if (!quotaNode || !usageNode || !storagePanel || !storageQuota || !storageUsage) { + // If anything of this is not available, we do not need to do anything more + return + } + + // do stuff + storagePanel.classList.remove('hidden') + const quotaValue = quotaNode.value + storageQuota.innerText = quotaValue + const usageValue = usageNode.value + storageUsage.innerText = usageValue } -})(solid) +})(solid, window.$rdf) diff --git a/default-templates/new-account/index.html b/default-templates/new-account/index.html index 7fe96bfa7..5c401dc61 100644 --- a/default-templates/new-account/index.html +++ b/default-templates/new-account/index.html @@ -69,6 +69,20 @@

Account settings

+ + From 803de27a720ec746c6eb083a1a74ae9ca35e1a35 Mon Sep 17 00:00:00 2001 From: Arne Hassel Date: Tue, 22 Jan 2019 14:14:19 +0100 Subject: [PATCH 06/12] Implemented function that prints bytes in nice format --- common/js/auth-buttons.js | 22 ++++++++++++++++++++-- 1 file changed, 20 insertions(+), 2 deletions(-) diff --git a/common/js/auth-buttons.js b/common/js/auth-buttons.js index 137d086e8..33868a45e 100644 --- a/common/js/auth-buttons.js +++ b/common/js/auth-buttons.js @@ -74,8 +74,26 @@ // do stuff storagePanel.classList.remove('hidden') const quotaValue = quotaNode.value - storageQuota.innerText = quotaValue + storageQuota.innerText = formatBytes(quotaValue) const usageValue = usageNode.value - storageUsage.innerText = usageValue + storageUsage.innerText = formatBytes(usageValue) + } + + function formatBytes (number, units = [ + 'B', + 'kB', + 'MB', + 'GB', + 'TB', + 'PB', + 'EB', + 'ZB', + 'YB' + ]) { + const unit = units.shift() + if (number > 1000 && units.length > 0) { + return formatBytes(number / 1000, units) + } + return `${Math.round(number * 100) / 100} ${unit}` } })(solid, window.$rdf) From 8ebda5d87f314b281e58936e086f1bc33bd20ee2 Mon Sep 17 00:00:00 2001 From: Arne Hassel Date: Tue, 22 Jan 2019 14:24:52 +0100 Subject: [PATCH 07/12] Added progress bar to the mix --- common/js/auth-buttons.js | 7 ++++++- default-templates/new-account/index.html | 5 +++++ 2 files changed, 11 insertions(+), 1 deletion(-) diff --git a/common/js/auth-buttons.js b/common/js/auth-buttons.js index 33868a45e..5db39a36d 100644 --- a/common/js/auth-buttons.js +++ b/common/js/auth-buttons.js @@ -66,7 +66,8 @@ const storagePanel = document.getElementById('StoragePanel') const storageQuota = document.getElementById('StorageQuota') const storageUsage = document.getElementById('StorageUsage') - if (!quotaNode || !usageNode || !storagePanel || !storageQuota || !storageUsage) { + const storageUsedBar = document.getElementById('StorageUsedBar') + if (!quotaNode || !usageNode || !storagePanel || !storageQuota || !storageUsage || !storageUsedBar) { // If anything of this is not available, we do not need to do anything more return } @@ -77,6 +78,10 @@ storageQuota.innerText = formatBytes(quotaValue) const usageValue = usageNode.value storageUsage.innerText = formatBytes(usageValue) + const usagePercent = Math.floor(usageValue / quotaValue * 1000) / 10 + storageUsedBar.innerText = `${usagePercent}%` + storageUsedBar.setAttribute('aria-valuenow', usagePercent) + storageUsedBar.style.width = `${usagePercent}%` } function formatBytes (number, units = [ diff --git a/default-templates/new-account/index.html b/default-templates/new-account/index.html index 5c401dc61..b1da6afef 100644 --- a/default-templates/new-account/index.html +++ b/default-templates/new-account/index.html @@ -72,6 +72,11 @@

Account settings