-
Notifications
You must be signed in to change notification settings - Fork 304
Refactor acl checker to use solid-permissions lib #443
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
Changes from all commits
bf1c4b2
24fe64f
80e0675
3b0bc9f
cfb71d1
3f4a7bd
a831439
8930736
5b17170
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -2,6 +2,8 @@ | |
|
|
||
| const async = require('async') | ||
| const path = require('path') | ||
| const PermissionSet = require('solid-permissions').PermissionSet | ||
| const rdf = require('rdflib') | ||
| const url = require('url') | ||
|
|
||
| const DEFAULT_ACL_SUFFIX = '.acl' | ||
|
|
@@ -33,18 +35,11 @@ class ACLChecker { | |
| // Let's see if there is a file.. | ||
| self.fetch(acl, function (err, graph) { | ||
| if (err || !graph || graph.length === 0) { | ||
| // TODO | ||
| // If no file is found and we want to Control, | ||
| // we should not be able to do that! | ||
| // Control is only to Read and Write the current file! | ||
| // if (mode === 'Control') { | ||
| // return next(new Error('You can\'t Control an unexisting file')) | ||
| // } | ||
| if (err) debug('Error: ' + err) | ||
| accessType = 'defaultForNew' | ||
| return next() | ||
| } | ||
| self.findRule( | ||
| self.checkAccess( | ||
| graph, // The ACL graph | ||
| user, // The webId of the user | ||
| mode, // Read/Write/Append | ||
|
|
@@ -81,125 +76,72 @@ class ACLChecker { | |
| }) | ||
| } | ||
|
|
||
| findAgentClass (graph, user, mode, resource, acl, callback) { | ||
| const debug = this.debug | ||
| // Agent class statement | ||
| var agentClassStatements = graph.match(acl, | ||
| 'http://www.w3.org/ns/auth/acl#agentClass') | ||
| if (agentClassStatements.length === 0) { | ||
| return callback(false) | ||
| } | ||
| async.some( | ||
| agentClassStatements, | ||
| function (agentClassTriple, found) { | ||
| // Check for FOAF groups | ||
| debug('Found agentClass policy') | ||
| if (agentClassTriple.object.uri === 'http://xmlns.com/foaf/0.1/Agent') { | ||
| debug(mode + ' allowed access as FOAF agent') | ||
| return found(true) | ||
| } | ||
| return found(false) | ||
| }, | ||
| callback) | ||
| } | ||
|
|
||
| findRule (graph, user, mode, resource, accessType, acl, callback, options) { | ||
| /** | ||
| * Tests whether a graph (parsed .acl resource) allows a given operation | ||
| * for a given user. Calls the provided callback with `null` if the user | ||
| * has access, otherwise calls it with an error. | ||
| * @method checkAccess | ||
| * @param graph {Graph} Parsed RDF graph of current .acl resource | ||
| * @param user {String} WebID URI of the user accessing the resource | ||
| * @param mode {String} Access mode, e.g. 'Read', 'Write', etc. | ||
| * @param resource {String} URI of the resource being accessed | ||
| * @param accessType {String} One of `accessTo`, or `default` | ||
| * @param acl {String} URI of this current .acl resource | ||
| * @param callback {Function} | ||
| * @param options {Object} Options hashmap | ||
| * @param [options.origin] Request's `Origin:` header | ||
| * @param [options.host] Request's host URI (with protocol) | ||
| */ | ||
| checkAccess (graph, user, mode, resource, accessType, acl, callback, | ||
| options = {}) { | ||
| const debug = this.debug | ||
| if (!graph || graph.length === 0) { | ||
| debug('ACL ' + acl + ' is empty') | ||
| return callback(new Error('No policy found')) | ||
| return callback(new Error('No policy found - empty ACL')) | ||
| } | ||
| debug('Found policies in ' + acl) | ||
| // Check for mode | ||
| var statements = this.getMode(graph, mode) | ||
| if (mode === 'Append') { | ||
| statements = statements.concat(this.getMode(graph, 'Write')) | ||
| let isContainer = accessType.startsWith('default') | ||
| let aclOptions = { | ||
| aclSuffix: this.suffix, | ||
| graph: graph, | ||
| host: options.host, | ||
| origin: options.origin, | ||
| rdf: rdf, | ||
| strictOrigin: this.strictOrigin, | ||
| isAcl: (uri) => { return this.isAcl(uri) }, | ||
| aclUrlFor: (uri) => { return this.aclUrlFor(uri) } | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. at this point, can we not pass the entire ACL instance?
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. We could? I'd rather be explicit, though.. |
||
| } | ||
| var self = this | ||
| async.some( | ||
| statements, | ||
| function (statement, done) { | ||
| var statementSubject = statement.subject.uri | ||
| // Check for origin | ||
| var matchOrigin = self.matchOrigin(graph, statementSubject, | ||
| options.origin, options.host) | ||
| if (!matchOrigin) { | ||
| debug('The request does not match the origin') | ||
| return done(false) | ||
| } | ||
| // Check for accessTo/defaultForNew | ||
| if (!self.isAcl(resource) || accessType === 'defaultForNew') { | ||
| debug('Checking for accessType:' + accessType) | ||
| var accesses = self.matchAccessType(graph, statementSubject, accessType, | ||
| resource) | ||
| if (!accesses) { | ||
| debug('Cannot find accessType ' + accessType) | ||
| return done(false) | ||
| } | ||
| } | ||
| // Check for Agent | ||
| var agentStatements = [] | ||
| if (user) { | ||
| agentStatements = graph.match( | ||
| statementSubject, | ||
| 'http://www.w3.org/ns/auth/acl#agent', | ||
| user) | ||
| } | ||
| if (agentStatements.length) { | ||
| debug(mode + ' access allowed (as agent) for: ' + user) | ||
| return done(true) | ||
| } | ||
| debug('Inspect agentClass') | ||
| // Check for AgentClass | ||
| return self.findAgentClass(graph, user, mode, resource, statementSubject, | ||
| done) | ||
| }, | ||
| function (found) { | ||
| if (!found) { | ||
| let acls = new PermissionSet(resource, acl, isContainer, aclOptions) | ||
| acls.checkAccess(resource, user, mode) | ||
| .then(hasAccess => { | ||
| if (hasAccess) { | ||
| debug(`${mode} access permitted to ${user}`) | ||
| return callback() | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Since this method is called
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Yeah, now that it's refactored the findRule name doesn't make as much sense. I'll rename it to something like |
||
| } else { | ||
| debug(`${mode} access not permitted to ${user}`) | ||
| return callback(new Error('Acl found but policy not found')) | ||
| } | ||
| return callback(null) | ||
| }) | ||
| .catch(err => { | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Could you clarify the distinction between a rejected promise and a resolved promise with a falsy value in
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Sure thing. |
||
| debug(`${mode} access denied to ${user}`) | ||
| debug(err) | ||
| return callback(err) | ||
| }) | ||
| } | ||
|
|
||
| getMode (graph, mode) { | ||
| return graph.match( | ||
| null, | ||
| 'http://www.w3.org/ns/auth/acl#mode', | ||
| 'http://www.w3.org/ns/auth/acl#' + mode | ||
| ) | ||
| aclUrlFor (uri) { | ||
| if (this.isAcl(uri)) { | ||
| return uri | ||
| } else { | ||
| return uri + this.suffix | ||
| } | ||
| } | ||
|
|
||
| isAcl (resource) { | ||
| return resource.endsWith(this.suffix) | ||
| } | ||
|
|
||
| matchAccessType (graph, rule, accessType, uri) { | ||
| var matches = graph.match( | ||
| rule, | ||
| 'http://www.w3.org/ns/auth/acl#' + accessType | ||
| ) | ||
| return matches.some(function (match) { | ||
| return uri.startsWith(match.object.uri) | ||
| }) | ||
| } | ||
|
|
||
| matchOrigin (graph, rule, origin, host) { | ||
| // if there is no origin, then the host is the origin | ||
| if (this.strictOrigin && !origin) { | ||
| return true | ||
| } | ||
| var origins = graph.match( | ||
| rule, | ||
| 'http://www.w3.org/ns/auth/acl#origin' | ||
| ) | ||
| if (origins.length) { | ||
| return origins.some(function (triple) { | ||
| return triple.object.uri === (origin || host) | ||
| }) | ||
| if (typeof resource === 'string') { | ||
| return resource.endsWith(this.suffix) | ||
| } else { | ||
| return false | ||
| } | ||
| // return true if origin is not enforced | ||
| return !this.strictOrigin | ||
| } | ||
|
|
||
| static possibleACLs (uri, suffix) { | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,65 @@ | ||
| 'use strict' | ||
| const proxyquire = require('proxyquire') | ||
| const assert = require('chai').assert | ||
| const debug = require('../lib/debug').ACL | ||
|
|
||
| class PermissionSetAlwaysGrant { | ||
| checkAccess () { | ||
| return Promise.resolve(true) | ||
| } | ||
| } | ||
| class PermissionSetNeverGrant { | ||
| checkAccess () { | ||
| return Promise.resolve(false) | ||
| } | ||
| } | ||
| class PermissionSetAlwaysError { | ||
| checkAccess () { | ||
| return Promise.reject(new Error('Error thrown during checkAccess()')) | ||
| } | ||
| } | ||
|
|
||
| describe('ACLChecker unit test', () => { | ||
| it('should callback with null on grant success', done => { | ||
| let ACLChecker = proxyquire('../lib/acl-checker', { | ||
| 'solid-permissions': { PermissionSet: PermissionSetAlwaysGrant } | ||
| }) | ||
| let graph = {} | ||
| let accessType = '' | ||
| let user, mode, resource, aclUrl | ||
| let acl = new ACLChecker({ debug }) | ||
| acl.checkAccess(graph, user, mode, resource, accessType, aclUrl, (err) => { | ||
| assert.isUndefined(err, | ||
| 'Granted permission should result in an empty callback!') | ||
| done() | ||
| }) | ||
| }) | ||
| it('should callback with error on grant failure', done => { | ||
| let ACLChecker = proxyquire('../lib/acl-checker', { | ||
| 'solid-permissions': { PermissionSet: PermissionSetNeverGrant } | ||
| }) | ||
| let graph = {} | ||
| let accessType = '' | ||
| let user, mode, resource, aclUrl | ||
| let acl = new ACLChecker({ debug }) | ||
| acl.checkAccess(graph, user, mode, resource, accessType, aclUrl, (err) => { | ||
| assert.ok(err instanceof Error, | ||
| 'Denied permission should result in an error callback!') | ||
| done() | ||
| }) | ||
| }) | ||
| it('should callback with error on grant error', done => { | ||
| let ACLChecker = proxyquire('../lib/acl-checker', { | ||
| 'solid-permissions': { PermissionSet: PermissionSetAlwaysError } | ||
| }) | ||
| let graph = {} | ||
| let accessType = '' | ||
| let user, mode, resource, aclUrl | ||
| let acl = new ACLChecker({ debug }) | ||
| acl.checkAccess(graph, user, mode, resource, accessType, aclUrl, (err) => { | ||
| assert.ok(err instanceof Error, | ||
| 'Error during checkAccess should result in an error callback!') | ||
| done() | ||
| }) | ||
| }) | ||
| }) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
how is this used?
where is
hostset and what is this set to?There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
hostgets set in theallowhandler, here.It's used by the 'enforce strict origin' code in the permissions lib.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
perfect! works with me!