44const { dirname } = require ( 'path' )
55const rdf = require ( 'rdflib' )
66const debug = require ( './debug' ) . ACL
7- const debugCache = require ( './debug' ) . cache
7+ // const debugCache = require('./debug').cache
88const HTTPError = require ( './http-error' )
99const aclCheck = require ( '@solid/acl-check' )
1010const { URL } = require ( 'url' )
@@ -55,68 +55,98 @@ class ACLChecker {
5555 }
5656 this . messagesCached [ cacheKey ] = this . messagesCached [ cacheKey ] || [ ]
5757
58- const acl = await this . getNearestACL ( ) . catch ( err => {
58+ // for method DELETE nearestACL and ACL from parent resource
59+ const acl = await this . getNearestACL ( method ) . catch ( err => {
5960 this . messagesCached [ cacheKey ] . push ( new HTTPError ( err . status || 500 , err . message || err ) )
6061 } )
6162 if ( ! acl ) {
6263 this . aclCached [ cacheKey ] = Promise . resolve ( false )
6364 return this . aclCached [ cacheKey ]
6465 }
6566 let resource = rdf . sym ( this . resource )
67+ let parentResource = resource
68+ if ( ! this . resource . endsWith ( '/' ) ) { parentResource = rdf . sym ( ACLChecker . getDirectory ( this . resource ) ) }
6669 if ( this . resource . endsWith ( '/' + this . suffix ) ) {
6770 resource = rdf . sym ( ACLChecker . getDirectory ( this . resource ) )
71+ parentResource = resource
6872 }
6973 // If this is an ACL, Control mode must be present for any operations
7074 if ( this . isAcl ( this . resource ) ) {
7175 mode = 'Control'
72- resource = rdf . sym ( this . resource . substring ( 0 , this . resource . length - this . suffix . length ) )
76+ const thisResource = this . resource . substring ( 0 , this . resource . length - this . suffix . length )
77+ resource = rdf . sym ( thisResource )
78+ parentResource = resource
79+ if ( ! thisResource . endsWith ( '/' ) ) parentResource = rdf . sym ( ACLChecker . getDirectory ( thisResource ) )
7380 }
74- // If the slug is an acl, reject
75- /* if (this.isAcl(this.slug)) {
76- this.aclCached[cacheKey] = Promise.resolve(false)
77- return this.aclCached[cacheKey]
78- } */
79- const directory = acl . isContainer ? rdf . sym ( ACLChecker . getDirectory ( acl . acl ) ) : null
80- const aclFile = rdf . sym ( acl . acl )
81+ const directory = acl . isContainer ? rdf . sym ( ACLChecker . getDirectory ( acl . docAcl ) ) : null
82+ const aclFile = rdf . sym ( acl . docAcl )
83+ const aclGraph = acl . docGraph
8184 const agent = user ? rdf . sym ( user ) : null
8285 const modes = [ ACL ( mode ) ]
8386 const agentOrigin = this . agentOrigin
8487 const trustedOrigins = this . trustedOrigins
8588 let originTrustedModes = [ ]
8689 try {
8790 this . fetch ( aclFile . doc ( ) . value )
88- originTrustedModes = await aclCheck . getTrustedModesForOrigin ( acl . graph , resource , directory , aclFile , agentOrigin , ( uriNode ) => {
89- return this . fetch ( uriNode . doc ( ) . value , acl . graph )
91+ originTrustedModes = await aclCheck . getTrustedModesForOrigin ( aclGraph , resource , directory , aclFile , agentOrigin , ( uriNode ) => {
92+ return this . fetch ( uriNode . doc ( ) . value , aclGraph )
9093 } )
9194 } catch ( e ) {
9295 // FIXME: https://github.com/solid/acl-check/issues/23
9396 // console.error(e.message)
9497 }
95- let accessDenied = aclCheck . accessDenied ( acl . graph , resource , directory , aclFile , agent , modes , agentOrigin , trustedOrigins , originTrustedModes )
9698
97- function accessDeniedForAccessTo ( mode ) {
98- const accessDeniedAccessTo = aclCheck . accessDenied ( acl . graph , directory , null , aclFile , agent , [ ACL ( mode ) ] , agentOrigin , trustedOrigins , originTrustedModes )
99+ function resourceAccessDenied ( modes ) {
100+ return aclCheck . accessDenied ( aclGraph , resource , directory , aclFile , agent , modes , agentOrigin , trustedOrigins , originTrustedModes )
101+ }
102+ function accessDeniedForAccessTo ( modes ) {
103+ const accessDeniedAccessTo = aclCheck . accessDenied ( aclGraph , directory , null , aclFile , agent , modes , agentOrigin , trustedOrigins , originTrustedModes )
99104 const accessResult = ! accessDenied && ! accessDeniedAccessTo
100- accessDenied = accessResult ? false : accessDenied || accessDeniedAccessTo
101- // debugCache('accessDenied result ' + accessDenied)
105+ return accessResult ? false : accessDenied || accessDeniedAccessTo
106+ }
107+ async function accessdeniedFromParent ( modes ) {
108+ const parentAclDirectory = ACLChecker . getDirectory ( acl . parentAcl )
109+ const parentDirectory = parentResource === parentAclDirectory ? null : rdf . sym ( parentAclDirectory )
110+ const accessDeniedParent = aclCheck . accessDenied ( acl . parentGraph , parentResource , parentDirectory , rdf . sym ( acl . parentAcl ) , agent , modes , agentOrigin , trustedOrigins , originTrustedModes )
111+ const accessResult = ! accessDenied && ! accessDeniedParent
112+ return accessResult ? false : accessDenied || accessDeniedParent
102113 }
114+
115+ let accessDenied = resourceAccessDenied ( modes )
116+ // debugCache('accessDenied resource ' + accessDenied)
117+
103118 // For create and update HTTP methods
104- if ( ( method === 'PUT' || method === 'PATCH' || method === 'COPY' ) && directory ) {
119+ if ( ( method === 'PUT' || method === 'PATCH' || method === 'COPY' ) ) {
105120 // if resource and acl have same parent container,
106121 // and resource does not exist, then accessTo Append from parent is required
107- if ( directory . value === dirname ( aclFile . value ) + '/' && ! resourceExists ) {
108- accessDeniedForAccessTo ( 'Append' )
122+ if ( directory && directory . value === dirname ( aclFile . value ) + '/' && ! resourceExists ) {
123+ accessDenied = accessDeniedForAccessTo ( [ ACL ( 'Append' ) ] )
109124 }
125+ // debugCache('accessDenied PUT/PATCH ' + accessDenied)
110126 }
111127
112128 // For delete HTTP method
113- if ( ( method === 'DELETE' ) && directory ) {
114- // if resource and acl have same parent container,
115- // then accessTo Write from parent is required
116- if ( directory . value === dirname ( aclFile . value ) + '/' ) {
117- accessDeniedForAccessTo ( 'Write' )
118- }
129+ if ( ( method === 'DELETE' ) ) {
130+ if ( resourceExists ) {
131+ // deleting a Container
132+ // without Read, the response code will reveal whether a Container is empty or not
133+ if ( directory && this . resource . endsWith ( '/' ) ) accessDenied = resourceAccessDenied ( [ ACL ( 'Read' ) , ACL ( 'Write' ) ] )
134+ // if resource and acl have same parent container,
135+ // then both Read and Write on parent is required
136+ else if ( ! directory && aclFile . value . endsWith ( `/${ this . suffix } ` ) ) accessDenied = await accessdeniedFromParent ( [ ACL ( 'Read' ) , ACL ( 'Write' ) ] )
137+
138+ // deleting a Document
139+ else if ( directory && directory . value === dirname ( aclFile . value ) + '/' ) {
140+ accessDenied = accessDeniedForAccessTo ( [ ACL ( 'Write' ) ] )
141+ } else {
142+ accessDenied = await accessdeniedFromParent ( [ ACL ( 'Write' ) ] )
143+ }
144+
145+ // https://github.com/solid/specification/issues/14#issuecomment-1712773516
146+ } else { accessDenied = true }
147+ // debugCache('accessDenied DELETE ' + accessDenied)
119148 }
149+
120150 if ( accessDenied && user ) {
121151 this . messagesCached [ cacheKey ] . push ( HTTPError ( 403 , accessDenied ) )
122152 } else if ( accessDenied ) {
@@ -140,43 +170,74 @@ class ACLChecker {
140170 return `${ parts . join ( '/' ) } /`
141171 }
142172
143- // Gets the ACL that applies to the resource
144- async getNearestACL ( ) {
173+ // Gets any ACLs that apply to the resource
174+ // DELETE uses docAcl when docAcl is parent to the resource
175+ // or docAcl and parentAcl when docAcl is the ACL of the Resource
176+ async getNearestACL ( method ) {
145177 const { resource } = this
146178 let isContainer = false
147179 const possibleACLs = this . getPossibleACLs ( )
148180 const acls = [ ...possibleACLs ]
149181 let returnAcl = null
150- while ( possibleACLs . length > 0 && ! returnAcl ) {
182+ let returnParentAcl = null
183+ let parentAcl = null
184+ let parentGraph = null
185+ let docAcl = null
186+ let docGraph = null
187+ while ( possibleACLs . length > 0 && ! returnParentAcl ) {
151188 const acl = possibleACLs . shift ( )
152189 let graph
153190 try {
154191 this . requests [ acl ] = this . requests [ acl ] || this . fetch ( acl )
155192 graph = await this . requests [ acl ]
156193 } catch ( err ) {
157194 if ( err && ( err . code === 'ENOENT' || err . status === 404 ) ) {
158- isContainer = true
195+ // only set isContainer before docAcl
196+ if ( ! docAcl ) isContainer = true
159197 continue
160198 }
161199 debug ( err )
162200 throw err
163201 }
164- const relative = resource . replace ( acl . replace ( / [ ^ / ] + $ / , '' ) , './' )
165- debug ( `Using ACL ${ acl } for ${ relative } ` )
166- returnAcl = { acl, graph, isContainer }
202+ // const relative = resource.replace(acl.replace(/[^/]+$/, ''), './')
203+ // debug(`Using ACL ${acl} for ${relative}`)
204+ if ( ! docAcl ) {
205+ docAcl = acl
206+ docGraph = graph
207+ // parentAcl is only needed for DELETE
208+ if ( method !== 'DELETE' ) returnParentAcl = true
209+ } else {
210+ parentAcl = acl
211+ parentGraph = graph
212+ returnParentAcl = true
213+ }
214+
215+ returnAcl = { docAcl, docGraph, isContainer, parentAcl, parentGraph }
167216 }
168217 if ( ! returnAcl ) {
169218 throw new HTTPError ( 500 , `No ACL found for ${ resource } , searched in \n- ${ acls . join ( '\n- ' ) } ` )
170219 }
171- const groupNodes = returnAcl . graph . statementsMatching ( null , ACL ( 'agentGroup' ) , null )
172- const groupUrls = groupNodes . map ( node => node . object . value . split ( '#' ) [ 0 ] )
220+ // fetch group
221+ let groupNodes = returnAcl . docGraph . statementsMatching ( null , ACL ( 'agentGroup' ) , null )
222+ let groupUrls = groupNodes . map ( node => node . object . value . split ( '#' ) [ 0 ] )
173223 await Promise . all ( groupUrls . map ( async groupUrl => {
174224 try {
175- const graph = await this . fetch ( groupUrl , returnAcl . graph )
176- this . requests [ groupUrl ] = this . requests [ groupUrl ] || graph
225+ const docGraph = await this . fetch ( groupUrl , returnAcl . docGraph )
226+ this . requests [ groupUrl ] = this . requests [ groupUrl ] || docGraph
177227 } catch ( e ) { } // failed to fetch groupUrl
178228 } ) )
229+ if ( parentAcl ) {
230+ groupNodes = returnAcl . parentGraph . statementsMatching ( null , ACL ( 'agentGroup' ) , null )
231+ groupUrls = groupNodes . map ( node => node . object . value . split ( '#' ) [ 0 ] )
232+ await Promise . all ( groupUrls . map ( async groupUrl => {
233+ try {
234+ const docGraph = await this . fetch ( groupUrl , returnAcl . parentGraph )
235+ this . requests [ groupUrl ] = this . requests [ groupUrl ] || docGraph
236+ } catch ( e ) { } // failed to fetch groupUrl
237+ } ) )
238+ }
179239
240+ // debugAccounts('ALAIN returnACl ' + '\ndocAcl ' + returnAcl.docAcl + '\nparentAcl ' + returnAcl.parentAcl)
180241 return returnAcl
181242 }
182243
@@ -264,7 +325,7 @@ function fetchLocalOrRemote (mapper, serverUri) {
264325 // debugCache('Expunging from cache', url)
265326 delete temporaryCache [ url ]
266327 if ( Object . keys ( temporaryCache ) . length === 0 ) {
267- debugCache ( 'Cache is empty again' )
328+ // debugCache('Cache is empty again')
268329 }
269330 } , EXPIRY_MS ) ,
270331 promise : doFetch ( url )
0 commit comments