Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add hasPaid(agent) function to ACL check #1577

Open
wants to merge 16 commits into
base: main
Choose a base branch
from
Open
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
3 changes: 2 additions & 1 deletion config/defaults.js
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,8 @@ module.exports = {
webid: true,
strictOrigin: true,
trustedOrigins: [],
dataBrowserPath: 'default'
dataBrowserPath: 'default',
// paymentOracle: 'http://localhost:8402'

// For use in Enterprises to configure a HTTP proxy for all outbound HTTP requests from the SOLID server (we use
// https://www.npmjs.com/package/global-tunnel-ng).
Expand Down
49 changes: 46 additions & 3 deletions lib/acl-checker.js
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,29 @@ const ACL = rdf.Namespace('http://www.w3.org/ns/auth/acl#')
const EXPIRY_MS = parseInt(process.env.ACL_CACHE_TIME) || 10000 // 10 seconds
let temporaryCache = {}

class PaymentOracleClient {
constructor (oracleEndpoint) {
this.oracleEndpoint = oracleEndpoint
}

async hasPaid (agent, resource) {
if (!this.oracleEndpoint) {
return false
}
try {
const agentStr = (agent ? encodeURIComponent(agent.value) : '')
const resourceStr = (resource ? encodeURIComponent(resource.value) : '')
const url = `${this.oracleEndpoint}/?agent=${agentStr}&resource=${resourceStr}`
const response = await httpFetch(url)
const body = await response.text()
return JSON.parse(body)
} catch (e) {
// console.error('Oracle failure', e.message)
return { ok: false }
}
}
}

// An ACLChecker exposes the permissions on a specific resource
class ACLChecker {
constructor (resource, options = {}) {
Expand All @@ -37,6 +60,7 @@ class ACLChecker {
this.messagesCached = {}
this.requests = {}
this.slug = options.slug
this.paymentOracleClient = options.paymentOracleClient
}

// Returns a fulfilled promise when the user can access the resource
Expand Down Expand Up @@ -72,10 +96,13 @@ class ACLChecker {
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 currentPaid = await this.paymentOracleClient.hasPaid(agent, rdf.sym(this.resource))

const modes = [ACL(mode)]
const agentOrigin = this.agentOrigin
const trustedOrigins = this.trustedOrigins
let originTrustedModes = []
let payingWouldHelp = false
try {
this.fetch(aclFile.doc().value)
originTrustedModes = await aclCheck.getTrustedModesForOrigin(acl.graph, resource, directory, aclFile, agentOrigin, (uriNode) => {
Expand All @@ -85,20 +112,35 @@ class ACLChecker {
// FIXME: https://github.com/solid/acl-check/issues/23
// console.error(e.message)
}
let accessDenied = aclCheck.accessDenied(acl.graph, resource, directory, aclFile, agent, modes, agentOrigin, trustedOrigins, originTrustedModes)

let accessDenied = aclCheck.accessDenied(acl.graph, resource, directory, aclFile, agent, modes, agentOrigin, trustedOrigins, originTrustedModes, (agent) => {
if (!currentPaid.ok) {
payingWouldHelp = currentPaid
}
return currentPaid.ok
})

// For create and update HTTP methods
if ((method === 'PUT' || method === 'PATCH' || method === 'COPY') && directory) {
// if resource and acl have same parent container,
// and resource does not exist, then accessTo Append from parent is required
if (directory.value === dirname(aclFile.value) + '/' && !resourceExists) {
const accessDeniedAccessTo = aclCheck.accessDenied(acl.graph, directory, null, aclFile, agent, [ACL('Append')], agentOrigin, trustedOrigins, originTrustedModes)
const accessDeniedAccessTo = aclCheck.accessDenied(acl.graph, directory, null, aclFile, agent, [ACL('Append')], agentOrigin, trustedOrigins, originTrustedModes, () => {
if (!currentPaid) {
payingWouldHelp = currentPaid
}
return currentPaid
})
const accessResult = !accessDenied && !accessDeniedAccessTo
accessDenied = accessResult ? false : accessDenied || accessDeniedAccessTo
// debugCache('accessDenied result ' + accessDenied)
}
}
if (accessDenied && user) {
if (accessDenied && payingWouldHelp) {
const err = HTTPError(402, 'Payment Required')
err.payHeaders = currentPaid.payHeaders
this.messagesCached[cacheKey].push(err)
} else if (accessDenied && user) {
this.messagesCached[cacheKey].push(HTTPError(403, accessDenied))
} else if (accessDenied) {
this.messagesCached[cacheKey].push(HTTPError(401, 'Unauthenticated'))
Expand Down Expand Up @@ -199,6 +241,7 @@ class ACLChecker {
suffix: ldp.suffixAcl,
strictOrigin: ldp.strictOrigin,
trustedOrigins,
paymentOracleClient: new PaymentOracleClient(ldp.paymentOracle),
slug: decodeURIComponent(req.headers.slug)
})
}
Expand Down
7 changes: 7 additions & 0 deletions lib/handlers/allow.js
Original file line number Diff line number Diff line change
Expand Up @@ -76,6 +76,13 @@ function allow (mode) {
if (resourceUrl.endsWith('.acl') && userId === await ldp.getOwner(req.hostname)) return next()

const error = req.authError || await req.acl.getError(userId, mode)
if (error.status === 402) {
if (Array.isArray(error.payHeaders)) {
error.payHeaders.forEach(str => {
res.set('Pay', str)
})
}
}
debug(`${mode} access denied to ${userId || '(none)'}: ${error.status} - ${error.message}`)
next(error)
}
Expand Down
1 change: 1 addition & 0 deletions lib/server-config.js
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ function printDebugInfo (options) {
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)
debug.settings('Payment Oracle for web monetization: ' + options.paymentOracle)
}

/**
Expand Down