@fleetbase/solid-engine 0.0.4 → 0.0.6
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/ACL_SOLUTION.md +72 -0
- package/CSS_SCOPE_ISSUE.md +140 -0
- package/HOTFIX_SYNTAX_ERROR.md +100 -0
- package/MANUAL_ACL_SETUP.md +135 -0
- package/REFACTORING_SUMMARY.md +330 -0
- package/VERIFICATION_CHECKLIST.md +82 -0
- package/addon/components/modals/create-solid-folder.hbs +29 -0
- package/addon/components/modals/import-solid-resources.hbs +85 -0
- package/addon/controllers/data/content.js +17 -0
- package/addon/controllers/data/index.js +219 -0
- package/addon/controllers/home.js +84 -0
- package/addon/engine.js +1 -24
- package/addon/extension.js +26 -0
- package/addon/routes/data/content.js +11 -0
- package/addon/routes/data/index.js +17 -0
- package/addon/routes.js +2 -7
- package/addon/styles/solid-engine.css +1 -2
- package/addon/templates/account.hbs +3 -3
- package/addon/templates/application.hbs +2 -12
- package/addon/templates/data/content.hbs +48 -0
- package/addon/templates/{pods/explorer.hbs → data/index.hbs} +6 -5
- package/addon/templates/home.hbs +139 -10
- package/app/components/modals/{backup-pod.js → create-solid-folder.js} +1 -1
- package/app/components/modals/{resync-pod.js → import-solid-resources.js} +1 -1
- package/app/components/modals/{create-pod.js → setup-css-credentials.js} +1 -1
- package/composer.json +4 -10
- package/extension.json +4 -2
- package/index.js +0 -11
- package/package.json +9 -8
- package/server/migrations/2024_12_21_add_css_credentials_to_solid_identities_table.php +32 -0
- package/server/src/Client/OpenIDConnectClient.php +686 -15
- package/server/src/Client/SolidClient.php +104 -8
- package/server/src/Http/Controllers/DataController.php +318 -0
- package/server/src/Http/Controllers/OIDCController.php +42 -8
- package/server/src/Http/Controllers/SolidController.php +179 -85
- package/server/src/Models/SolidIdentity.php +13 -3
- package/server/src/Services/AclService.php +405 -0
- package/server/src/Services/PodService.php +910 -0
- package/server/src/Services/ResourceSyncService.php +344 -0
- package/server/src/Services/VehicleSyncService.php +289 -0
- package/server/src/Support/Utils.php +10 -0
- package/server/src/routes.php +25 -1
- package/addon/components/modals/backup-pod.hbs +0 -3
- package/addon/components/modals/create-pod.hbs +0 -5
- package/addon/components/modals/resync-pod.hbs +0 -3
- package/addon/controllers/pods/explorer/content.js +0 -12
- package/addon/controllers/pods/explorer.js +0 -149
- package/addon/controllers/pods/index/pod.js +0 -12
- package/addon/controllers/pods/index.js +0 -137
- package/addon/routes/pods/explorer/content.js +0 -10
- package/addon/routes/pods/explorer.js +0 -44
- package/addon/routes/pods/index/pod.js +0 -3
- package/addon/routes/pods/index.js +0 -21
- package/addon/templates/pods/explorer/content.hbs +0 -19
- package/addon/templates/pods/index/pod.hbs +0 -11
- package/addon/templates/pods/index.hbs +0 -19
- package/server/src/LegacyClient/Identity/IdentityProvider.php +0 -174
- package/server/src/LegacyClient/Identity/Profile.php +0 -18
- package/server/src/LegacyClient/OIDCClient.php +0 -350
- package/server/src/LegacyClient/Profile/WebID.php +0 -26
- package/server/src/LegacyClient/SolidClient.php +0 -271
|
@@ -0,0 +1,405 @@
|
|
|
1
|
+
<?php
|
|
2
|
+
|
|
3
|
+
namespace Fleetbase\Solid\Services;
|
|
4
|
+
|
|
5
|
+
use Fleetbase\Solid\Models\SolidIdentity;
|
|
6
|
+
use Illuminate\Support\Facades\Log;
|
|
7
|
+
|
|
8
|
+
class AclService
|
|
9
|
+
{
|
|
10
|
+
protected PodService $podService;
|
|
11
|
+
|
|
12
|
+
public function __construct(PodService $podService)
|
|
13
|
+
{
|
|
14
|
+
$this->podService = $podService;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Check if the pod root has write/append permissions
|
|
19
|
+
*/
|
|
20
|
+
public function hasWritePermissions(SolidIdentity $identity, string $podUrl): bool
|
|
21
|
+
{
|
|
22
|
+
try {
|
|
23
|
+
$response = $identity->request('get', $podUrl);
|
|
24
|
+
|
|
25
|
+
if (!$response->successful()) {
|
|
26
|
+
Log::warning('[ACL CHECK FAILED]', [
|
|
27
|
+
'pod_url' => $podUrl,
|
|
28
|
+
'status' => $response->status(),
|
|
29
|
+
]);
|
|
30
|
+
return false;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
$wacAllow = $response->header('WAC-Allow');
|
|
34
|
+
Log::info('[ACL CHECK]', [
|
|
35
|
+
'pod_url' => $podUrl,
|
|
36
|
+
'wac_allow' => $wacAllow,
|
|
37
|
+
]);
|
|
38
|
+
|
|
39
|
+
// Check if WAC-Allow header includes write or append
|
|
40
|
+
if ($wacAllow) {
|
|
41
|
+
return str_contains($wacAllow, 'write') || str_contains($wacAllow, 'append');
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
return false;
|
|
45
|
+
} catch (\Exception $e) {
|
|
46
|
+
Log::error('[ACL CHECK ERROR]', [
|
|
47
|
+
'pod_url' => $podUrl,
|
|
48
|
+
'error' => $e->getMessage(),
|
|
49
|
+
]);
|
|
50
|
+
return false;
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* Update the pod root ACL to grant write permissions
|
|
56
|
+
*/
|
|
57
|
+
public function grantWritePermissions(SolidIdentity $identity, string $podUrl, string $webId): bool
|
|
58
|
+
{
|
|
59
|
+
try {
|
|
60
|
+
$aclUrl = rtrim($podUrl, '/') . '/.acl';
|
|
61
|
+
|
|
62
|
+
// Generate ACL Turtle document
|
|
63
|
+
$aclTurtle = $this->generateAclDocument($podUrl, $webId);
|
|
64
|
+
|
|
65
|
+
Log::info('[ACL UPDATE]', [
|
|
66
|
+
'acl_url' => $aclUrl,
|
|
67
|
+
'webid' => $webId,
|
|
68
|
+
]);
|
|
69
|
+
|
|
70
|
+
// PUT the ACL document
|
|
71
|
+
$response = $identity->request('put', $aclUrl, $aclTurtle, [
|
|
72
|
+
'headers' => [
|
|
73
|
+
'Content-Type' => 'text/turtle',
|
|
74
|
+
],
|
|
75
|
+
]);
|
|
76
|
+
|
|
77
|
+
if ($response->successful()) {
|
|
78
|
+
Log::info('[ACL UPDATED]', [
|
|
79
|
+
'acl_url' => $aclUrl,
|
|
80
|
+
'status' => $response->status(),
|
|
81
|
+
]);
|
|
82
|
+
return true;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
Log::error('[ACL UPDATE FAILED]', [
|
|
86
|
+
'acl_url' => $aclUrl,
|
|
87
|
+
'status' => $response->status(),
|
|
88
|
+
'body' => $response->body(),
|
|
89
|
+
]);
|
|
90
|
+
return false;
|
|
91
|
+
|
|
92
|
+
} catch (\Exception $e) {
|
|
93
|
+
Log::error('[ACL UPDATE ERROR]', [
|
|
94
|
+
'pod_url' => $podUrl,
|
|
95
|
+
'error' => $e->getMessage(),
|
|
96
|
+
'trace' => $e->getTraceAsString(),
|
|
97
|
+
]);
|
|
98
|
+
return false;
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
/**
|
|
103
|
+
* Generate ACL Turtle document
|
|
104
|
+
*/
|
|
105
|
+
protected function generateAclDocument(string $podUrl, string $webId): string
|
|
106
|
+
{
|
|
107
|
+
$podUrl = rtrim($podUrl, '/') . '/';
|
|
108
|
+
|
|
109
|
+
return <<<TURTLE
|
|
110
|
+
@prefix acl: <http://www.w3.org/ns/auth/acl#>.
|
|
111
|
+
|
|
112
|
+
# Full rights for the pod owner
|
|
113
|
+
<#owner>
|
|
114
|
+
a acl:Authorization;
|
|
115
|
+
acl:agent <{$webId}>;
|
|
116
|
+
acl:accessTo <{$podUrl}>;
|
|
117
|
+
acl:default <{$podUrl}>;
|
|
118
|
+
acl:mode acl:Read, acl:Write, acl:Control.
|
|
119
|
+
|
|
120
|
+
# Append and read rights for Fleetbase integration
|
|
121
|
+
<#fleetbase>
|
|
122
|
+
a acl:Authorization;
|
|
123
|
+
acl:agent <{$webId}>;
|
|
124
|
+
acl:accessTo <{$podUrl}>;
|
|
125
|
+
acl:default <{$podUrl}>;
|
|
126
|
+
acl:mode acl:Append, acl:Read.
|
|
127
|
+
|
|
128
|
+
TURTLE;
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
/**
|
|
132
|
+
* Ensure pod has write permissions, update ACL if needed
|
|
133
|
+
*/
|
|
134
|
+
public function ensureWritePermissions(SolidIdentity $identity, string $podUrl, string $webId): bool
|
|
135
|
+
{
|
|
136
|
+
// Check if already has write permissions
|
|
137
|
+
if ($this->hasWritePermissions($identity, $podUrl)) {
|
|
138
|
+
Log::info('[ACL OK]', ['pod_url' => $podUrl]);
|
|
139
|
+
return true;
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
// Need to update ACL
|
|
143
|
+
Log::info('[ACL NEEDS UPDATE]', ['pod_url' => $podUrl]);
|
|
144
|
+
return $this->grantWritePermissions($identity, $podUrl, $webId);
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
/**
|
|
148
|
+
* Ensure a folder has proper ACL permissions after creation.
|
|
149
|
+
*
|
|
150
|
+
* @param SolidIdentity $identity
|
|
151
|
+
* @param string $folderUrl The folder URL (must end with /)
|
|
152
|
+
* @param string $webId The WebID to grant permissions to
|
|
153
|
+
* @return bool
|
|
154
|
+
*/
|
|
155
|
+
public function ensureFolderPermissions(SolidIdentity $identity, string $folderUrl, string $webId): bool
|
|
156
|
+
{
|
|
157
|
+
try {
|
|
158
|
+
// Ensure folder URL ends with /
|
|
159
|
+
$folderUrl = rtrim($folderUrl, '/') . '/';
|
|
160
|
+
$aclUrl = $folderUrl . '.acl';
|
|
161
|
+
|
|
162
|
+
Log::info('[ACL] Ensuring folder permissions', [
|
|
163
|
+
'folder_url' => $folderUrl,
|
|
164
|
+
'acl_url' => $aclUrl,
|
|
165
|
+
'webid' => $webId,
|
|
166
|
+
]);
|
|
167
|
+
|
|
168
|
+
// Check if ACL already exists and has write permissions
|
|
169
|
+
if ($this->hasFolderWritePermissions($identity, $folderUrl, $webId)) {
|
|
170
|
+
Log::info('[ACL] Folder already has write permissions', ['folder_url' => $folderUrl]);
|
|
171
|
+
return true;
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
// Create ACL with full permissions for the owner
|
|
175
|
+
$aclContent = $this->generateFolderAcl($folderUrl, $webId);
|
|
176
|
+
|
|
177
|
+
return $this->createFolderAcl($identity, $aclUrl, $aclContent);
|
|
178
|
+
} catch (\Throwable $e) {
|
|
179
|
+
Log::error('[ACL] Failed to ensure folder permissions', [
|
|
180
|
+
'folder_url' => $folderUrl,
|
|
181
|
+
'error' => $e->getMessage(),
|
|
182
|
+
]);
|
|
183
|
+
return false;
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
/**
|
|
188
|
+
* Check if a folder has write permissions.
|
|
189
|
+
*
|
|
190
|
+
* @param SolidIdentity $identity
|
|
191
|
+
* @param string $folderUrl
|
|
192
|
+
* @param string $webId
|
|
193
|
+
* @return bool
|
|
194
|
+
*/
|
|
195
|
+
protected function hasFolderWritePermissions(SolidIdentity $identity, string $folderUrl, string $webId): bool
|
|
196
|
+
{
|
|
197
|
+
try {
|
|
198
|
+
$response = $identity->request('head', $folderUrl);
|
|
199
|
+
|
|
200
|
+
if (!$response->successful()) {
|
|
201
|
+
return false;
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
// Check WAC-Allow header
|
|
205
|
+
$wacAllow = $response->header('WAC-Allow');
|
|
206
|
+
|
|
207
|
+
if ($wacAllow) {
|
|
208
|
+
Log::debug('[ACL] WAC-Allow header', [
|
|
209
|
+
'folder_url' => $folderUrl,
|
|
210
|
+
'wac_allow' => $wacAllow,
|
|
211
|
+
]);
|
|
212
|
+
|
|
213
|
+
// Parse WAC-Allow header: user="read write", public="read"
|
|
214
|
+
if (preg_match('/user="([^"]*)"/i', $wacAllow, $matches)) {
|
|
215
|
+
$userModes = strtolower($matches[1]);
|
|
216
|
+
return str_contains($userModes, 'write') || str_contains($userModes, 'append');
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
return false;
|
|
221
|
+
} catch (\Throwable $e) {
|
|
222
|
+
Log::debug('[ACL] Error checking folder permissions', [
|
|
223
|
+
'folder_url' => $folderUrl,
|
|
224
|
+
'error' => $e->getMessage(),
|
|
225
|
+
]);
|
|
226
|
+
return false;
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
/**
|
|
231
|
+
* Create an ACL file for a folder.
|
|
232
|
+
*
|
|
233
|
+
* @param SolidIdentity $identity
|
|
234
|
+
* @param string $aclUrl
|
|
235
|
+
* @param string $aclContent
|
|
236
|
+
* @return bool
|
|
237
|
+
*/
|
|
238
|
+
protected function createFolderAcl(SolidIdentity $identity, string $aclUrl, string $aclContent): bool
|
|
239
|
+
{
|
|
240
|
+
try {
|
|
241
|
+
$response = $identity->request('put', $aclUrl, $aclContent, [
|
|
242
|
+
'headers' => [
|
|
243
|
+
'Content-Type' => 'text/turtle',
|
|
244
|
+
],
|
|
245
|
+
]);
|
|
246
|
+
|
|
247
|
+
if ($response->successful()) {
|
|
248
|
+
Log::info('[ACL] Folder ACL created successfully', [
|
|
249
|
+
'acl_url' => $aclUrl,
|
|
250
|
+
'status' => $response->status(),
|
|
251
|
+
]);
|
|
252
|
+
return true;
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
Log::error('[ACL] Failed to create folder ACL', [
|
|
256
|
+
'acl_url' => $aclUrl,
|
|
257
|
+
'status' => $response->status(),
|
|
258
|
+
'body' => $response->body(),
|
|
259
|
+
]);
|
|
260
|
+
|
|
261
|
+
return false;
|
|
262
|
+
} catch (\Throwable $e) {
|
|
263
|
+
Log::error('[ACL] Error creating folder ACL', [
|
|
264
|
+
'acl_url' => $aclUrl,
|
|
265
|
+
'error' => $e->getMessage(),
|
|
266
|
+
]);
|
|
267
|
+
return false;
|
|
268
|
+
}
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
/**
|
|
272
|
+
* Generate ACL content for a folder with full owner permissions.
|
|
273
|
+
*
|
|
274
|
+
* @param string $folderUrl The folder URL
|
|
275
|
+
* @param string $webId The WebID to grant permissions to
|
|
276
|
+
* @return string
|
|
277
|
+
*/
|
|
278
|
+
protected function generateFolderAcl(string $folderUrl, string $webId): string
|
|
279
|
+
{
|
|
280
|
+
return <<<TURTLE
|
|
281
|
+
@prefix acl: <http://www.w3.org/ns/auth/acl#>.
|
|
282
|
+
|
|
283
|
+
<#owner>
|
|
284
|
+
a acl:Authorization;
|
|
285
|
+
acl:agent <{$webId}>;
|
|
286
|
+
acl:accessTo <./>;
|
|
287
|
+
acl:default <./>;
|
|
288
|
+
acl:mode acl:Read, acl:Write, acl:Control.
|
|
289
|
+
TURTLE;
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
/**
|
|
293
|
+
* Check if a specific URL is writable (has write or append permissions).
|
|
294
|
+
*
|
|
295
|
+
* @param SolidIdentity $identity
|
|
296
|
+
* @param string $url
|
|
297
|
+
* @return bool
|
|
298
|
+
*/
|
|
299
|
+
public function isWritable(SolidIdentity $identity, string $url): bool
|
|
300
|
+
{
|
|
301
|
+
try {
|
|
302
|
+
$response = $identity->request('head', $url);
|
|
303
|
+
|
|
304
|
+
if (!$response->successful()) {
|
|
305
|
+
return false;
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
$wacAllow = $response->header('WAC-Allow');
|
|
309
|
+
|
|
310
|
+
if ($wacAllow && preg_match('/user="([^"]*)"/i', $wacAllow, $matches)) {
|
|
311
|
+
$modes = strtolower($matches[1]);
|
|
312
|
+
$isWritable = str_contains($modes, 'write') || str_contains($modes, 'append');
|
|
313
|
+
|
|
314
|
+
Log::debug('[ACL] Writable check', [
|
|
315
|
+
'url' => $url,
|
|
316
|
+
'wac_allow' => $wacAllow,
|
|
317
|
+
'is_writable' => $isWritable,
|
|
318
|
+
]);
|
|
319
|
+
|
|
320
|
+
return $isWritable;
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
return false;
|
|
324
|
+
} catch (\Throwable $e) {
|
|
325
|
+
Log::debug('[ACL] Writable check failed', [
|
|
326
|
+
'url' => $url,
|
|
327
|
+
'error' => $e->getMessage(),
|
|
328
|
+
]);
|
|
329
|
+
return false;
|
|
330
|
+
}
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
/**
|
|
334
|
+
* Find writable storage locations from user profile.
|
|
335
|
+
*
|
|
336
|
+
* @param SolidIdentity $identity
|
|
337
|
+
* @param array $profile
|
|
338
|
+
* @return array
|
|
339
|
+
*/
|
|
340
|
+
public function findWritableLocations(SolidIdentity $identity, array $profile): array
|
|
341
|
+
{
|
|
342
|
+
$writableLocations = [];
|
|
343
|
+
$webId = $profile['webid'] ?? null;
|
|
344
|
+
|
|
345
|
+
if (!$webId) {
|
|
346
|
+
return $writableLocations;
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
// Get pod URL from WebID
|
|
350
|
+
$podUrl = $this->podService->getPodUrlFromWebId($webId);
|
|
351
|
+
|
|
352
|
+
// Check common storage locations
|
|
353
|
+
$commonLocations = [
|
|
354
|
+
'public' => rtrim($podUrl, '/') . '/public/',
|
|
355
|
+
'private' => rtrim($podUrl, '/') . '/private/',
|
|
356
|
+
'inbox' => rtrim($podUrl, '/') . '/inbox/',
|
|
357
|
+
];
|
|
358
|
+
|
|
359
|
+
foreach ($commonLocations as $name => $url) {
|
|
360
|
+
if ($this->isWritable($identity, $url)) {
|
|
361
|
+
$writableLocations[$name] = $url;
|
|
362
|
+
}
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
// Check storage locations from profile
|
|
366
|
+
foreach ($profile['storage_locations'] ?? [] as $storage) {
|
|
367
|
+
$storageUrl = $this->resolveStorageUrl($storage, $webId, $podUrl);
|
|
368
|
+
if ($storageUrl && $this->isWritable($identity, $storageUrl)) {
|
|
369
|
+
$writableLocations['storage_' . count($writableLocations)] = $storageUrl;
|
|
370
|
+
}
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
Log::info('[ACL] Found writable locations', [
|
|
374
|
+
'count' => count($writableLocations),
|
|
375
|
+
'locations' => $writableLocations,
|
|
376
|
+
]);
|
|
377
|
+
|
|
378
|
+
return $writableLocations;
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
/**
|
|
382
|
+
* Resolve a storage URL from profile data.
|
|
383
|
+
*
|
|
384
|
+
* @param string $storage
|
|
385
|
+
* @param string $webId
|
|
386
|
+
* @param string $podUrl
|
|
387
|
+
* @return string|null
|
|
388
|
+
*/
|
|
389
|
+
protected function resolveStorageUrl(string $storage, string $webId, string $podUrl): ?string
|
|
390
|
+
{
|
|
391
|
+
// Handle relative URLs
|
|
392
|
+
if ($storage === '../' || $storage === './') {
|
|
393
|
+
return $podUrl;
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
// Handle absolute URLs
|
|
397
|
+
if (str_starts_with($storage, 'http://') || str_starts_with($storage, 'https://')) {
|
|
398
|
+
return $storage;
|
|
399
|
+
}
|
|
400
|
+
|
|
401
|
+
// Handle relative paths
|
|
402
|
+
$webIdBase = dirname($webId);
|
|
403
|
+
return rtrim($webIdBase, '/') . '/' . ltrim($storage, '/');
|
|
404
|
+
}
|
|
405
|
+
}
|