Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
63 changes: 61 additions & 2 deletions common/js/auth-buttons.js
Original file line number Diff line number Diff line change
@@ -1,13 +1,17 @@
/* 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 =>
document.getElementById(id) || document.createElement('a'))
loginButton.addEventListener('click', login)
logoutButton.addEventListener('click', logout)
registerButton.addEventListener('click', register)
if ($rdf) {
document.addEventListener('DOMContentLoaded', getAccountInfo)
}

// Track authentication status and update UI
auth.trackSession(session => {
Expand Down Expand Up @@ -46,4 +50,59 @@
registration.searchParams.set('returnToUrl', location)
location.href = registration
}
})(solid)

async function getAccountInfo () {
const SOLID = $rdf.Namespace('http://www.w3.org/ns/solid/terms#')

// get DOM and data-dependencies
const storagePanel = document.getElementById('StoragePanel')
const storageQuota = document.getElementById('StorageQuota')
const storageUsage = document.getElementById('StorageUsage')
const storageUsedBar = document.getElementById('StorageUsedBar')
if (!storagePanel || !storageQuota || !storageUsage || !storageUsedBar) {
// If anything of these elements are not available, we do not need to do anything more
return
}

// load data
const accountUrl = new URL('/account', location).href
const accountStore = $rdf.graph()
const accountFetcher = new $rdf.Fetcher(accountStore)
await accountFetcher.load(accountUrl)
const quotaNode = accountStore.any($rdf.sym(accountUrl), SOLID('storageQuota'))
const usageNode = accountStore.any($rdf.sym(accountUrl), SOLID('storageUsage'))
if (!quotaNode || !usageNode) {
// If data is not available, we do not need to do anything more
return
}

// manipulate DOM to present data
storagePanel.classList.remove('hidden')
const quotaValue = quotaNode.value
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 = [
'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)
19 changes: 19 additions & 0 deletions default-templates/new-account/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -69,6 +69,25 @@ <h1>Account settings</h1>
</a>
</div>
</section>

<section class="hidden" id="StoragePanel">
<h1>Storage</h1>
<div class="progress">
<div id="StorageUsedBar" class="progress-bar" role="progressbar" aria-valuenow="0" aria-valuemin="0" aria-valuemax="100" style="width: 0%; min-width: 3em;">
0%
</div>
</div>
<ul class="list-group">
<li class="list-group-item">
<span class="lead">Storage used:</span>
<span id="StorageUsage">0</span>
</li>
<li class="list-group-item">
<span class="lead">Storage available:</span>
<span id="StorageQuota">0</span>
</li>
</ul>
</section>
</div>

<script src="/common/js/solid-auth-client.bundle.js"></script>
Expand Down
45 changes: 45 additions & 0 deletions lib/acl-checker.js
Original file line number Diff line number Diff line change
Expand Up @@ -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#')
Expand Down Expand Up @@ -144,6 +146,49 @@ class ACLChecker {
isAcl (resource) {
return resource.endsWith(this.suffix)
}

Comment thread
kjetilk marked this conversation as resolved.
static createFromLDPAndRequest (resource, ldp, req) {

@megoth megoth Jan 23, 2019 •

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This could be moved as a method to ldp.js instead, something like createACL(resource, req) if that's a better abstraction? @kjetilk @rubensworks

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think it's fine to keep it here, but not strong opinion.

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
Expand Down
3 changes: 3 additions & 0 deletions lib/api/accounts/user-accounts.js
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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)

Expand Down
48 changes: 1 addition & 47 deletions lib/handlers/allow.js
Original file line number Diff line number Diff line change
@@ -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) {
Expand Down Expand Up @@ -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
Expand All @@ -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
}
}
8 changes: 8 additions & 0 deletions lib/ldp.js
Original file line number Diff line number Diff line change
Expand Up @@ -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)
}
Expand Down
83 changes: 83 additions & 0 deletions lib/requests/get-account-info-request.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
'use strict'

const ACLChecker = require('../acl-checker')
const AuthRequest = require('./auth-request')
const { actualSize } = require('../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 = 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)
}
return res.status(200).set('Content-Type', contentType).send(body)
}
}

async function getBody (req, contentType) {
const userId = req.session.userId
if (!userId) {
return new HTTPError(401, 'Requires authentication')
}
const ldp = req.app.locals.ldp
const rootUrl = `${ldp.resourceMapper.resolveUrl(req.hostname)}/`
const acl = ACLChecker.createFromLDPAndRequest(rootUrl, ldp, req)
const isAllowed = await acl.can(userId, 'Control')
if (!isAllowed) {
return await req.acl.getError(userId, 'Control')
}

// Dereference graph for webId
let userIdGraph
try {
userIdGraph = await ldp.getGraph(userId)
} catch (err) {
return err
}

// Get object value for pim:storage
const storageUrlNode = userIdGraph.any($rdf.sym(userId), PIM('storage'))
if (!storageUrlNode) {
return new HTTPError(404, 'Unable to find pim:storage for webId')
}

// Dereference graph for serverSide.ttl
const serverSideUrl = `${storageUrlNode.value}settings/serverSide.ttl` // needs to be hardcoded for now
let serverSideGraph
try {
serverSideGraph = await ldp.getGraph(serverSideUrl)
} catch (err) {
return err
}

// Get object value for solid:storageQuota
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()
store.add(storageUrlNode, SOLID('storageUsage'), $rdf.lit(size))
store.add(storageUrlNode, SOLID('storageQuota'), quotaNode)
return await new Promise((resolve, reject) => {
$rdf.serialize(null, store, storageUrlNode.value, contentType, (err, result) => {
if (err) {
return reject(err)
}
resolve(result)
})
})
}

module.exports = GetAccountInfoRequest
1 change: 1 addition & 0 deletions lib/utils.js
Original file line number Diff line number Diff line change
Expand Up @@ -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')
Expand Down
Loading