Skip to content

Commit 8751bfc

Browse files
Add WAC (Web Access Control) support (v0.0.4)
Features: - .acl Link header discovery (rel="acl") - WAC parser for JSON-LD ACL documents - WAC checker with mode and agent matching - Default ACL files on pod creation: - Root: owner full, public read - Private: owner only - Settings: owner only - Inbox: owner full, public append Tests: 56 passing (14 new WAC tests)
1 parent c9683f0 commit 8751bfc

8 files changed

Lines changed: 819 additions & 20 deletions

File tree

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "javascript-solid-server",
3-
"version": "0.0.3",
3+
"version": "0.0.4",
44
"description": "A minimal, fast Solid server",
55
"main": "src/index.js",
66
"type": "module",

src/handlers/container.js

Lines changed: 18 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2,9 +2,7 @@ import * as storage from '../storage/filesystem.js';
22
import { getAllHeaders } from '../ldp/headers.js';
33
import { isContainer } from '../utils/url.js';
44
import { generateProfile, generatePreferences, generateTypeIndex, serialize } from '../webid/profile.js';
5-
6-
// Content type for profile card
7-
const PROFILE_CONTENT_TYPE = 'text/html';
5+
import { generateOwnerAcl, generatePrivateAcl, generateInboxAcl, serializeAcl } from '../wac/parser.js';
86

97
/**
108
* Handle POST request to container (create new resource)
@@ -133,6 +131,23 @@ export async function handleCreatePod(request, reply) {
133131
const privateTypeIndex = generateTypeIndex(`${podUri}settings/privateTypeIndex`);
134132
await storage.write(`${podPath}settings/privateTypeIndex`, serialize(privateTypeIndex));
135133

134+
// Create default ACL files
135+
// Pod root: owner full control, public read
136+
const rootAcl = generateOwnerAcl(podUri, webId, true);
137+
await storage.write(`${podPath}.acl`, serializeAcl(rootAcl));
138+
139+
// Private folder: owner only (no public)
140+
const privateAcl = generatePrivateAcl(`${podUri}private/`, webId);
141+
await storage.write(`${podPath}private/.acl`, serializeAcl(privateAcl));
142+
143+
// Settings folder: owner only
144+
const settingsAcl = generatePrivateAcl(`${podUri}settings/`, webId);
145+
await storage.write(`${podPath}settings/.acl`, serializeAcl(settingsAcl));
146+
147+
// Inbox: owner full, public append
148+
const inboxAcl = generateInboxAcl(`${podUri}inbox/`, webId);
149+
await storage.write(`${podPath}inbox/.acl`, serializeAcl(inboxAcl));
150+
136151
} catch (err) {
137152
console.error('Pod creation error:', err);
138153
// Cleanup on failure

src/handlers/resource.js

Lines changed: 19 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ export async function handleGet(request, reply) {
1515
}
1616

1717
const origin = request.headers.origin;
18+
const resourceUrl = `${request.protocol}://${request.hostname}${urlPath}`;
1819

1920
// Handle container
2021
if (stats.isDirectory) {
@@ -31,7 +32,8 @@ export async function handleGet(request, reply) {
3132
isContainer: true,
3233
etag: indexStats?.etag || stats.etag,
3334
contentType: 'text/html',
34-
origin
35+
origin,
36+
resourceUrl
3537
});
3638

3739
Object.entries(headers).forEach(([k, v]) => reply.header(k, v));
@@ -40,14 +42,14 @@ export async function handleGet(request, reply) {
4042

4143
// No index.html, return JSON-LD container listing
4244
const entries = await storage.listContainer(urlPath);
43-
const baseUrl = `${request.protocol}://${request.hostname}${urlPath}`;
44-
const jsonLd = generateContainerJsonLd(baseUrl, entries || []);
45+
const jsonLd = generateContainerJsonLd(resourceUrl, entries || []);
4546

4647
const headers = getAllHeaders({
4748
isContainer: true,
4849
etag: stats.etag,
4950
contentType: 'application/ld+json',
50-
origin
51+
origin,
52+
resourceUrl
5153
});
5254

5355
Object.entries(headers).forEach(([k, v]) => reply.header(k, v));
@@ -65,7 +67,8 @@ export async function handleGet(request, reply) {
6567
isContainer: false,
6668
etag: stats.etag,
6769
contentType,
68-
origin
70+
origin,
71+
resourceUrl
6972
});
7073

7174
Object.entries(headers).forEach(([k, v]) => reply.header(k, v));
@@ -84,13 +87,15 @@ export async function handleHead(request, reply) {
8487
}
8588

8689
const origin = request.headers.origin;
90+
const resourceUrl = `${request.protocol}://${request.hostname}${urlPath}`;
8791
const contentType = stats.isDirectory ? 'application/ld+json' : getContentType(urlPath);
8892

8993
const headers = getAllHeaders({
9094
isContainer: stats.isDirectory,
9195
etag: stats.etag,
9296
contentType,
93-
origin
97+
origin,
98+
resourceUrl
9499
});
95100

96101
if (!stats.isDirectory) {
@@ -135,8 +140,9 @@ export async function handlePut(request, reply) {
135140
}
136141

137142
const origin = request.headers.origin;
138-
const headers = getAllHeaders({ isContainer: false, origin });
139-
headers['Location'] = `${request.protocol}://${request.hostname}${urlPath}`;
143+
const resourceUrl = `${request.protocol}://${request.hostname}${urlPath}`;
144+
const headers = getAllHeaders({ isContainer: false, origin, resourceUrl });
145+
headers['Location'] = resourceUrl;
140146

141147
Object.entries(headers).forEach(([k, v]) => reply.header(k, v));
142148
return reply.code(existed ? 204 : 201).send();
@@ -159,7 +165,8 @@ export async function handleDelete(request, reply) {
159165
}
160166

161167
const origin = request.headers.origin;
162-
const headers = getAllHeaders({ isContainer: false, origin });
168+
const resourceUrl = `${request.protocol}://${request.hostname}${urlPath}`;
169+
const headers = getAllHeaders({ isContainer: false, origin, resourceUrl });
163170
Object.entries(headers).forEach(([k, v]) => reply.header(k, v));
164171

165172
return reply.code(204).send();
@@ -173,9 +180,11 @@ export async function handleOptions(request, reply) {
173180
const stats = await storage.stat(urlPath);
174181

175182
const origin = request.headers.origin;
183+
const resourceUrl = `${request.protocol}://${request.hostname}${urlPath}`;
176184
const headers = getAllHeaders({
177185
isContainer: stats?.isDirectory || isContainer(urlPath),
178-
origin
186+
origin,
187+
resourceUrl
179188
});
180189

181190
Object.entries(headers).forEach(([k, v]) => reply.header(k, v));

src/ldp/headers.js

Lines changed: 31 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -7,28 +7,53 @@ const LDP = 'http://www.w3.org/ns/ldp#';
77
/**
88
* Get Link headers for a resource
99
* @param {boolean} isContainer
10+
* @param {string} aclUrl - URL to the ACL resource
1011
* @returns {string}
1112
*/
12-
export function getLinkHeader(isContainer) {
13+
export function getLinkHeader(isContainer, aclUrl = null) {
1314
const links = [`<${LDP}Resource>; rel="type"`];
1415

1516
if (isContainer) {
1617
links.push(`<${LDP}Container>; rel="type"`);
1718
links.push(`<${LDP}BasicContainer>; rel="type"`);
1819
}
1920

21+
// Add acl link for auxiliary resource discovery
22+
if (aclUrl) {
23+
links.push(`<${aclUrl}>; rel="acl"`);
24+
}
25+
2026
return links.join(', ');
2127
}
2228

29+
/**
30+
* Get the ACL URL for a resource
31+
* @param {string} resourceUrl - Full URL of the resource
32+
* @param {boolean} isContainer - Whether the resource is a container
33+
* @returns {string} ACL URL
34+
*/
35+
export function getAclUrl(resourceUrl, isContainer) {
36+
if (isContainer) {
37+
// Container ACL: /path/.acl
38+
const base = resourceUrl.endsWith('/') ? resourceUrl : resourceUrl + '/';
39+
return base + '.acl';
40+
}
41+
// Resource ACL: /path/file.acl
42+
return resourceUrl + '.acl';
43+
}
44+
2345
/**
2446
* Get standard LDP response headers
2547
* @param {object} options
2648
* @returns {object}
2749
*/
28-
export function getResponseHeaders({ isContainer = false, etag = null, contentType = null }) {
50+
export function getResponseHeaders({ isContainer = false, etag = null, contentType = null, resourceUrl = null, wacAllow = null }) {
51+
// Calculate ACL URL if resource URL provided
52+
const aclUrl = resourceUrl ? getAclUrl(resourceUrl, isContainer) : null;
53+
2954
const headers = {
30-
'Link': getLinkHeader(isContainer),
31-
'WAC-Allow': 'user="read write append control", public="read write append"',
55+
'Link': getLinkHeader(isContainer, aclUrl),
56+
'WAC-Allow': wacAllow || 'user="read write append control", public="read write append"',
3257
'Accept-Patch': 'application/sparql-update',
3358
'Allow': 'GET, HEAD, PUT, DELETE, OPTIONS' + (isContainer ? ', POST' : ''),
3459
'Vary': 'Accept, Authorization, Origin'
@@ -70,9 +95,9 @@ export function getCorsHeaders(origin) {
7095
* @param {object} options
7196
* @returns {object}
7297
*/
73-
export function getAllHeaders({ isContainer = false, etag = null, contentType = null, origin = null }) {
98+
export function getAllHeaders({ isContainer = false, etag = null, contentType = null, origin = null, resourceUrl = null, wacAllow = null }) {
7499
return {
75-
...getResponseHeaders({ isContainer, etag, contentType }),
100+
...getResponseHeaders({ isContainer, etag, contentType, resourceUrl, wacAllow }),
76101
...getCorsHeaders(origin)
77102
};
78103
}

0 commit comments

Comments
 (0)