Skip to content

Commit a736338

Browse files
Resolve relative URLs in WAC ACL parser
The ACL parser now correctly resolves relative URLs like "./" and "../" in accessTo and default fields against the ACL document's base URL. This allows ACL files to use relative paths as per WAC spec. Added tests for relative URL resolution.
1 parent 97497b7 commit a736338

2 files changed

Lines changed: 114 additions & 5 deletions

File tree

src/wac/parser.js

Lines changed: 45 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -83,10 +83,48 @@ function isAuthorization(node) {
8383
);
8484
}
8585

86+
/**
87+
* Get base URL from ACL URL (the container the ACL applies to)
88+
* e.g., https://example.com/foo/.acl -> https://example.com/foo/
89+
* https://example.com/foo/bar.acl -> https://example.com/foo/
90+
*/
91+
function getBaseUrl(aclUrl) {
92+
if (!aclUrl) return null;
93+
// Remove .acl suffix and get the directory
94+
const withoutAcl = aclUrl.replace(/\.acl$/, '');
95+
// If it was a container ACL (ended with /.acl), withoutAcl ends with /
96+
// If it was a resource ACL (foo.acl), we need the parent directory
97+
if (withoutAcl.endsWith('/')) {
98+
return withoutAcl;
99+
}
100+
// Get parent directory
101+
const lastSlash = withoutAcl.lastIndexOf('/');
102+
return lastSlash > 0 ? withoutAcl.substring(0, lastSlash + 1) : withoutAcl;
103+
}
104+
105+
/**
106+
* Resolve a URI against a base URL
107+
*/
108+
function resolveUri(uri, baseUrl) {
109+
if (!uri || !baseUrl) return uri;
110+
// Already absolute
111+
if (uri.startsWith('http://') || uri.startsWith('https://')) return uri;
112+
// Fragment-only (like #owner) - not a resource URL
113+
if (uri.startsWith('#')) return uri;
114+
// Resolve relative URL
115+
try {
116+
return new URL(uri, baseUrl).href;
117+
} catch {
118+
return uri;
119+
}
120+
}
121+
86122
/**
87123
* Parse a single Authorization node
88124
*/
89125
function parseAuthorization(node, aclUrl) {
126+
const baseUrl = getBaseUrl(aclUrl);
127+
90128
const auth = {
91129
id: node['@id'],
92130
accessTo: [], // Resources this applies to
@@ -97,13 +135,15 @@ function parseAuthorization(node, aclUrl) {
97135
modes: [] // Access modes
98136
};
99137

100-
// Parse accessTo
101-
auth.accessTo = parseUriArray(node['acl:accessTo'] || node['accessTo']);
138+
// Parse accessTo - resolve relative URLs
139+
auth.accessTo = parseUriArray(node['acl:accessTo'] || node['accessTo'])
140+
.map(uri => resolveUri(uri, baseUrl));
102141

103-
// Parse default (for containers)
104-
auth.default = parseUriArray(node['acl:default'] || node['default']);
142+
// Parse default (for containers) - resolve relative URLs
143+
auth.default = parseUriArray(node['acl:default'] || node['default'])
144+
.map(uri => resolveUri(uri, baseUrl));
105145

106-
// Parse agents
146+
// Parse agents (WebIDs can be relative too)
107147
auth.agents = parseUriArray(node['acl:agent'] || node['agent']);
108148

109149
// Parse agentClass

test/wac.test.js

Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -98,6 +98,75 @@ describe('WAC Parser', () => {
9898
assert.ok(auths[0].modes.includes(AccessMode.READ));
9999
assert.ok(auths[0].modes.includes(AccessMode.WRITE));
100100
});
101+
102+
it('should resolve relative accessTo URLs', async () => {
103+
const acl = {
104+
'@context': { 'acl': 'http://www.w3.org/ns/auth/acl#' },
105+
'@id': '#owner',
106+
'@type': 'acl:Authorization',
107+
'acl:agent': { '@id': 'https://alice.example/#me' },
108+
'acl:accessTo': { '@id': './' },
109+
'acl:mode': [{ '@id': 'acl:Read' }]
110+
};
111+
112+
const auths = await parseAcl(JSON.stringify(acl), 'https://alice.example/folder/.acl');
113+
114+
assert.strictEqual(auths.length, 1);
115+
assert.ok(auths[0].accessTo.includes('https://alice.example/folder/'),
116+
`Expected accessTo to include 'https://alice.example/folder/', got: ${auths[0].accessTo}`);
117+
});
118+
119+
it('should resolve relative default URLs', async () => {
120+
const acl = {
121+
'@context': { 'acl': 'http://www.w3.org/ns/auth/acl#' },
122+
'@id': '#owner',
123+
'@type': 'acl:Authorization',
124+
'acl:agent': { '@id': 'https://alice.example/#me' },
125+
'acl:accessTo': { '@id': './' },
126+
'acl:default': { '@id': './' },
127+
'acl:mode': [{ '@id': 'acl:Read' }]
128+
};
129+
130+
const auths = await parseAcl(JSON.stringify(acl), 'https://alice.example/folder/.acl');
131+
132+
assert.strictEqual(auths.length, 1);
133+
assert.ok(auths[0].default.includes('https://alice.example/folder/'),
134+
`Expected default to include 'https://alice.example/folder/', got: ${auths[0].default}`);
135+
});
136+
137+
it('should resolve parent-relative URLs like ../other/', async () => {
138+
const acl = {
139+
'@context': { 'acl': 'http://www.w3.org/ns/auth/acl#' },
140+
'@id': '#owner',
141+
'@type': 'acl:Authorization',
142+
'acl:agent': { '@id': 'https://alice.example/#me' },
143+
'acl:accessTo': { '@id': '../other/' },
144+
'acl:mode': [{ '@id': 'acl:Read' }]
145+
};
146+
147+
const auths = await parseAcl(JSON.stringify(acl), 'https://alice.example/folder/.acl');
148+
149+
assert.strictEqual(auths.length, 1);
150+
assert.ok(auths[0].accessTo.includes('https://alice.example/other/'),
151+
`Expected accessTo to include 'https://alice.example/other/', got: ${auths[0].accessTo}`);
152+
});
153+
154+
it('should keep absolute URLs unchanged', async () => {
155+
const acl = {
156+
'@context': { 'acl': 'http://www.w3.org/ns/auth/acl#' },
157+
'@id': '#owner',
158+
'@type': 'acl:Authorization',
159+
'acl:agent': { '@id': 'https://alice.example/#me' },
160+
'acl:accessTo': { '@id': 'https://other.example/resource' },
161+
'acl:mode': [{ '@id': 'acl:Read' }]
162+
};
163+
164+
const auths = await parseAcl(JSON.stringify(acl), 'https://alice.example/folder/.acl');
165+
166+
assert.strictEqual(auths.length, 1);
167+
assert.ok(auths[0].accessTo.includes('https://other.example/resource'),
168+
`Expected accessTo to include 'https://other.example/resource', got: ${auths[0].accessTo}`);
169+
});
101170
});
102171

103172
describe('generateOwnerAcl', () => {

0 commit comments

Comments
 (0)