4
4
const { dirname } = require ( 'path' )
5
5
const rdf = require ( 'rdflib' )
6
6
const debug = require ( './debug' ) . ACL
7
- const debugCache = require ( './debug' ) . cache
7
+ // const debugCache = require('./debug').cache
8
8
const HTTPError = require ( './http-error' )
9
9
const aclCheck = require ( '@solid/acl-check' )
10
10
const { URL } = require ( 'url' )
@@ -55,68 +55,98 @@ class ACLChecker {
55
55
}
56
56
this . messagesCached [ cacheKey ] = this . messagesCached [ cacheKey ] || [ ]
57
57
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 => {
59
60
this . messagesCached [ cacheKey ] . push ( new HTTPError ( err . status || 500 , err . message || err ) )
60
61
} )
61
62
if ( ! acl ) {
62
63
this . aclCached [ cacheKey ] = Promise . resolve ( false )
63
64
return this . aclCached [ cacheKey ]
64
65
}
65
66
let resource = rdf . sym ( this . resource )
67
+ let parentResource = resource
68
+ if ( ! this . resource . endsWith ( '/' ) ) { parentResource = rdf . sym ( ACLChecker . getDirectory ( this . resource ) ) }
66
69
if ( this . resource . endsWith ( '/' + this . suffix ) ) {
67
70
resource = rdf . sym ( ACLChecker . getDirectory ( this . resource ) )
71
+ parentResource = resource
68
72
}
69
73
// If this is an ACL, Control mode must be present for any operations
70
74
if ( this . isAcl ( this . resource ) ) {
71
75
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 ) )
73
80
}
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
81
84
const agent = user ? rdf . sym ( user ) : null
82
85
const modes = [ ACL ( mode ) ]
83
86
const agentOrigin = this . agentOrigin
84
87
const trustedOrigins = this . trustedOrigins
85
88
let originTrustedModes = [ ]
86
89
try {
87
90
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 )
90
93
} )
91
94
} catch ( e ) {
92
95
// FIXME: https://github.com/solid/acl-check/issues/23
93
96
// console.error(e.message)
94
97
}
95
- let accessDenied = aclCheck . accessDenied ( acl . graph , resource , directory , aclFile , agent , modes , agentOrigin , trustedOrigins , originTrustedModes )
96
98
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 )
99
104
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
102
113
}
114
+
115
+ let accessDenied = resourceAccessDenied ( modes )
116
+ // debugCache('accessDenied resource ' + accessDenied)
117
+
103
118
// For create and update HTTP methods
104
- if ( ( method === 'PUT' || method === 'PATCH' || method === 'COPY' ) && directory ) {
119
+ if ( ( method === 'PUT' || method === 'PATCH' || method === 'COPY' ) ) {
105
120
// if resource and acl have same parent container,
106
121
// 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' ) ] )
109
124
}
125
+ // debugCache('accessDenied PUT/PATCH ' + accessDenied)
110
126
}
111
127
112
128
// 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)
119
148
}
149
+
120
150
if ( accessDenied && user ) {
121
151
this . messagesCached [ cacheKey ] . push ( HTTPError ( 403 , accessDenied ) )
122
152
} else if ( accessDenied ) {
@@ -140,43 +170,74 @@ class ACLChecker {
140
170
return `${ parts . join ( '/' ) } /`
141
171
}
142
172
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 ) {
145
177
const { resource } = this
146
178
let isContainer = false
147
179
const possibleACLs = this . getPossibleACLs ( )
148
180
const acls = [ ...possibleACLs ]
149
181
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 ) {
151
188
const acl = possibleACLs . shift ( )
152
189
let graph
153
190
try {
154
191
this . requests [ acl ] = this . requests [ acl ] || this . fetch ( acl )
155
192
graph = await this . requests [ acl ]
156
193
} catch ( err ) {
157
194
if ( err && ( err . code === 'ENOENT' || err . status === 404 ) ) {
158
- isContainer = true
195
+ // only set isContainer before docAcl
196
+ if ( ! docAcl ) isContainer = true
159
197
continue
160
198
}
161
199
debug ( err )
162
200
throw err
163
201
}
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 }
167
216
}
168
217
if ( ! returnAcl ) {
169
218
throw new HTTPError ( 500 , `No ACL found for ${ resource } , searched in \n- ${ acls . join ( '\n- ' ) } ` )
170
219
}
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 ] )
173
223
await Promise . all ( groupUrls . map ( async groupUrl => {
174
224
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
177
227
} catch ( e ) { } // failed to fetch groupUrl
178
228
} ) )
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
+ }
179
239
240
+ // debugAccounts('ALAIN returnACl ' + '\ndocAcl ' + returnAcl.docAcl + '\nparentAcl ' + returnAcl.parentAcl)
180
241
return returnAcl
181
242
}
182
243
@@ -264,7 +325,7 @@ function fetchLocalOrRemote (mapper, serverUri) {
264
325
// debugCache('Expunging from cache', url)
265
326
delete temporaryCache [ url ]
266
327
if ( Object . keys ( temporaryCache ) . length === 0 ) {
267
- debugCache ( 'Cache is empty again' )
328
+ // debugCache('Cache is empty again')
268
329
}
269
330
} , EXPIRY_MS ) ,
270
331
promise : doFetch ( url )
0 commit comments