11'use strict'
22
3- const PermissionSet = require ( 'solid-permissions' ) . PermissionSet
43const rdf = require ( 'rdflib' )
54const debug = require ( './debug' ) . ACL
65const HTTPError = require ( './http-error' )
6+ const aclCheck = require ( '@solid/acl-check' )
7+ const { URL } = require ( 'url' )
78
89const DEFAULT_ACL_SUFFIX = '.acl'
10+ const ACL = rdf . Namespace ( 'http://www.w3.org/ns/auth/acl#' )
911
1012// An ACLChecker exposes the permissions on a specific resource
1113class ACLChecker {
1214 constructor ( resource , options = { } ) {
1315 this . resource = resource
14- this . host = options . host
15- this . origin = options . origin
16+ this . resourceUrl = new URL ( resource )
17+ this . agentOrigin = options . agentOrigin
1618 this . fetch = options . fetch
1719 this . fetchGraph = options . fetchGraph
1820 this . strictOrigin = options . strictOrigin
19- this . originsAllowed = options . originsAllowed
21+ this . trustedOrigins = options . trustedOrigins
2022 this . suffix = options . suffix || DEFAULT_ACL_SUFFIX
23+ this . aclCached = { }
24+ this . messagesCached = { }
25+ this . requests = { }
2126 }
2227
2328 // Returns a fulfilled promise when the user can access the resource
2429 // in the given mode, or rejects with an HTTP error otherwise
25- can ( user , mode ) {
30+ async can ( user , mode ) {
31+ const cacheKey = `${ mode } -${ user } `
32+ if ( this . aclCached [ cacheKey ] ) {
33+ return this . aclCached [ cacheKey ]
34+ }
35+ this . messagesCached [ cacheKey ] = this . messagesCached [ cacheKey ] || [ ]
36+
37+ const acl = await this . getNearestACL ( ) . catch ( err => {
38+ this . messagesCached [ cacheKey ] . push ( new HTTPError ( err . status || 500 , err . message || err ) )
39+ } )
40+ if ( ! acl ) {
41+ this . aclCached [ cacheKey ] = Promise . resolve ( false )
42+ return this . aclCached [ cacheKey ]
43+ }
44+ let resource = rdf . sym ( this . resource )
45+ if ( this . resource . endsWith ( '/' + this . suffix ) ) {
46+ resource = rdf . sym ( ACLChecker . getDirectory ( this . resource ) )
47+ }
2648 // If this is an ACL, Control mode must be present for any operations
2749 if ( this . isAcl ( this . resource ) ) {
2850 mode = 'Control'
51+ resource = rdf . sym ( this . resource . substring ( 0 , this . resource . length - this . suffix . length ) )
2952 }
30-
31- // Obtain the permission set for the resource
32- if ( ! this . _permissionSet ) {
33- this . _permissionSet = this . getNearestACL ( )
34- . then ( acl => this . getPermissionSet ( acl ) )
53+ const directory = acl . isContainer ? rdf . sym ( ACLChecker . getDirectory ( acl . acl ) ) : null
54+ const aclFile = rdf . sym ( acl . acl )
55+ const agent = user ? rdf . sym ( user ) : null
56+ const modes = [ ACL ( mode ) ]
57+ const agentOrigin = this . agentOrigin ? rdf . sym ( this . agentOrigin ) : null
58+ const trustedOrigins = this . trustedOrigins ? this . trustedOrigins . map ( trustedOrigin => rdf . sym ( trustedOrigin ) ) : null
59+ const accessDenied = aclCheck . accessDenied ( acl . graph , resource , directory , aclFile , agent , modes , agentOrigin , trustedOrigins )
60+ if ( accessDenied && this . agentOrigin && this . resourceUrl . origin !== this . agentOrigin ) {
61+ this . messagesCached [ cacheKey ] . push ( new HTTPError ( 403 , accessDenied ) )
62+ } else if ( accessDenied && user ) {
63+ this . messagesCached [ cacheKey ] . push ( new HTTPError ( 403 , accessDenied ) )
64+ } else if ( accessDenied ) {
65+ this . messagesCached [ cacheKey ] . push ( new HTTPError ( 401 , accessDenied ) )
3566 }
67+ this . aclCached [ cacheKey ] = Promise . resolve ( ! accessDenied )
68+ return this . aclCached [ cacheKey ]
69+ }
3670
37- // Check the resource's permissions
38- return this . _permissionSet
39- . then ( acls => this . checkAccess ( acls , user , mode ) )
40- . catch ( ( ) => {
41- if ( ! user ) {
42- throw new HTTPError ( 401 , `Access to ${ this . resource } requires authorization` )
43- } else {
44- throw new HTTPError ( 403 , `Access to ${ this . resource } denied for ${ user } ` )
45- }
46- } )
71+ async getError ( user , mode ) {
72+ const cacheKey = `${ mode } -${ user } `
73+ this . aclCached [ cacheKey ] = this . aclCached [ cacheKey ] || this . can ( user , mode )
74+ const isAllowed = await this . aclCached [ cacheKey ]
75+ return isAllowed ? null : this . messagesCached [ cacheKey ] . reduce ( ( prevMsg , msg ) => msg . status > prevMsg . status ? msg : prevMsg , { status : 0 } )
76+ }
77+
78+ static getDirectory ( aclFile ) {
79+ const parts = aclFile . split ( '/' )
80+ parts . pop ( )
81+ return `${ parts . join ( '/' ) } /`
4782 }
4883
4984 // Gets the ACL that applies to the resource
50- getNearestACL ( ) {
85+ async getNearestACL ( ) {
5186 const { resource } = this
5287 let isContainer = false
53- // Create a cascade of reject handlers (one for each possible ACL)
54- const nearestACL = this . getPossibleACLs ( ) . reduce ( ( prevACL , acl ) => {
55- return prevACL . catch ( ( ) => new Promise ( ( resolve , reject ) => {
56- this . fetch ( acl , ( err , graph ) => {
57- if ( err || ! graph || ! graph . length ) {
58- isContainer = true
59- reject ( err )
60- } else {
61- const relative = resource . replace ( acl . replace ( / [ ^ / ] + $ / , '' ) , './' )
62- debug ( `Using ACL ${ acl } for ${ relative } ` )
63- resolve ( { acl, graph, isContainer } )
64- }
65- } )
66- } ) )
67- } , Promise . reject ( ) )
68- return nearestACL . catch ( e => { throw new Error ( 'No ACL resource found' ) } )
88+ const possibleACLs = this . getPossibleACLs ( )
89+ const acls = [ ...possibleACLs ]
90+ let returnAcl = null
91+ while ( possibleACLs . length > 0 && ! returnAcl ) {
92+ const acl = possibleACLs . shift ( )
93+ let graph
94+ try {
95+ this . requests [ acl ] = this . requests [ acl ] || this . fetch ( acl )
96+ graph = await this . requests [ acl ]
97+ } catch ( err ) {
98+ if ( err && ( err . code === 'ENOENT' || err . status === 404 ) ) {
99+ isContainer = true
100+ continue
101+ }
102+ debug ( err )
103+ throw err
104+ }
105+ const relative = resource . replace ( acl . replace ( / [ ^ / ] + $ / , '' ) , './' )
106+ debug ( `Using ACL ${ acl } for ${ relative } ` )
107+ returnAcl = { acl, graph, isContainer }
108+ }
109+ if ( ! returnAcl ) {
110+ throw new HTTPError ( 500 , `No ACL found for ${ resource } , searched in \n- ${ acls . join ( '\n- ' ) } ` )
111+ }
112+ const groupUrls = returnAcl . graph
113+ . statementsMatching ( null , ACL ( 'agentGroup' ) , null )
114+ . map ( node => node . object . value . split ( '#' ) [ 0 ] )
115+ await Promise . all ( groupUrls . map ( groupUrl => {
116+ this . requests [ groupUrl ] = this . requests [ groupUrl ] || this . fetch ( groupUrl , returnAcl . graph )
117+ return this . requests [ groupUrl ]
118+ } ) )
119+
120+ return returnAcl
69121 }
70122
71123 // Gets all possible ACL paths that apply to the resource
72124 getPossibleACLs ( ) {
73125 // Obtain the resource URI and the length of its base
74126 let { resource : uri , suffix } = this
75- const [ { length : base } ] = uri . match ( / ^ [ ^ : ] + : \/ * [ ^ / ] + / )
127+ const [ { length : base } ] = uri . match ( / ^ [ ^ : ] + : \/ * [ ^ / ] + / )
76128
77129 // If the URI points to a file, append the file's ACL
78130 const possibleAcls = [ ]
@@ -87,43 +139,6 @@ class ACLChecker {
87139 return possibleAcls
88140 }
89141
90- // Tests whether the permissions allow a given operation
91- checkAccess ( permissionSet , user , mode ) {
92- const options = { fetchGraph : this . fetchGraph }
93- return permissionSet . checkAccess ( this . resource , user , mode , options )
94- . then ( hasAccess => {
95- if ( hasAccess ) {
96- return true
97- } else {
98- throw new Error ( 'ACL file found but no matching policy found' )
99- }
100- } )
101- }
102-
103- // Gets the permission set for the given ACL
104- getPermissionSet ( { acl, graph, isContainer } ) {
105- if ( ! graph || graph . length === 0 ) {
106- debug ( 'ACL ' + acl + ' is empty' )
107- throw new Error ( 'No policy found - empty ACL' )
108- }
109- const aclOptions = {
110- aclSuffix : this . suffix ,
111- graph : graph ,
112- host : this . host ,
113- origin : this . origin ,
114- rdf : rdf ,
115- strictOrigin : this . strictOrigin ,
116- originsAllowed : this . originsAllowed ,
117- isAcl : uri => this . isAcl ( uri ) ,
118- aclUrlFor : uri => this . aclUrlFor ( uri )
119- }
120- return new PermissionSet ( this . resource , acl , isContainer , aclOptions )
121- }
122-
123- aclUrlFor ( uri ) {
124- return this . isAcl ( uri ) ? uri : uri + this . suffix
125- }
126-
127142 isAcl ( resource ) {
128143 return resource . endsWith ( this . suffix )
129144 }
0 commit comments