Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
37 changes: 30 additions & 7 deletions packages/connected-solid/src/resources/SolidResource.ts
Original file line number Diff line number Diff line change
Expand Up @@ -122,6 +122,12 @@ export abstract class SolidResource
*/
protected wacRule?: WacRule;

/**
* @internal
* If an acl:default wac rule was fetched, it is cached here
*/
protected inheritableWacRule?: WacRule;

/**
* @internal
* Handles notification subscriptions
Expand Down Expand Up @@ -743,7 +749,7 @@ export abstract class SolidResource
* @returns WAC Rules results
*/
protected async getWacUri(options?: {
ignoreCache: boolean;
ignoreCache?: boolean;
}): Promise<GetWacUriResult<SolidLeaf | SolidContainer>> {
const thisAsLeafOrContainer = this as unknown as SolidLeaf | SolidContainer;
// Get the wacUri if not already present
Expand Down Expand Up @@ -789,17 +795,25 @@ export abstract class SolidResource
* }
* ```
*/
async getWac(options?: {
ignoreCache: boolean;
async getWac(options?: { ignoreCache?: boolean }) {
return await this._getWac(options);
}

private async _getWac(options?: {
ignoreCache?: boolean;
inheritable?: boolean;
}): Promise<
| GetWacUriError<SolidContainer | SolidLeaf>
| GetWacRuleError<SolidContainer | SolidLeaf>
| GetWacRuleSuccess<SolidContainer | SolidLeaf>
> {
const thisAsLeafOrContainer = this as unknown as SolidLeaf | SolidContainer;
// Return the wac rule if it's already cached
if (!options?.ignoreCache && this.wacRule) {
return new GetWacRuleSuccess(thisAsLeafOrContainer, this.wacRule);
const cachedRule = options?.inheritable
? this.inheritableWacRule
: this.wacRule;
if (!options?.ignoreCache && cachedRule) {
return new GetWacRuleSuccess(thisAsLeafOrContainer, cachedRule);
}

// Get the wac uri
Expand All @@ -812,12 +826,17 @@ export abstract class SolidResource
thisAsLeafOrContainer,
{
fetch: this.context.solid.fetch,
inheritable: options?.inheritable,
},
);
if (wacResult.isError) return wacResult;
// If the wac rules was successfully found
if (wacResult.type === "getWacRuleSuccess") {
this.wacRule = wacResult.wacRule;
if (options?.inheritable) {
this.inheritableWacRule = wacResult.wacRule;
} else {
this.wacRule = wacResult.wacRule;
}
return wacResult;
}

Expand All @@ -830,7 +849,7 @@ export abstract class SolidResource
`Resource "${this.uri}" has no Effective ACL resource`,
);
}
return parentResource.getWac();
return parentResource._getWac({ ...options, inheritable: true });
}

/**
Expand Down Expand Up @@ -888,7 +907,11 @@ export abstract class SolidResource
this.emit("update");
return result;
}
// update cache
this.wacRule = result.wacRule;
// clear default rule cache
// to simplify logic
this.inheritableWacRule = undefined;
return result;
}

Expand Down
24 changes: 19 additions & 5 deletions packages/connected-solid/src/wac/getWacRule.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,9 @@ export type GetWacRuleResult<ResourceType extends SolidContainer | SolidLeaf> =
| GetWacRuleError<ResourceType>
| WacRuleAbsent<ResourceType>;

type GetWacRuleOptions<ResourceType extends SolidContainer | SolidLeaf> =
ResourceType extends SolidContainer ? { inheritable?: boolean } : never;

/**
* Given the URI of an ACL document, return the Web Access Control (WAC) rules
* @param aclUri: The URI for the ACL document
Expand All @@ -34,22 +37,22 @@ export type GetWacRuleResult<ResourceType extends SolidContainer | SolidLeaf> =
export async function getWacRuleWithAclUri(
aclUri: string,
resource: SolidContainer,
options?: BasicRequestOptions,
options?: BasicRequestOptions & GetWacRuleOptions<SolidContainer>,
): Promise<GetWacRuleResult<SolidContainer>>;
export async function getWacRuleWithAclUri(
aclUri: string,
resource: SolidLeaf,
options?: BasicRequestOptions,
options?: BasicRequestOptions & GetWacRuleOptions<SolidLeaf>,
): Promise<GetWacRuleResult<SolidLeaf>>;
export async function getWacRuleWithAclUri(
aclUri: string,
resource: SolidLeaf | SolidContainer,
options?: BasicRequestOptions,
options?: BasicRequestOptions & GetWacRuleOptions<SolidLeaf | SolidContainer>,
): Promise<GetWacRuleResult<SolidLeaf | SolidContainer>>;
export async function getWacRuleWithAclUri(
aclUri: string,
resource: SolidLeaf | SolidContainer,
options?: BasicRequestOptions,
options?: BasicRequestOptions & GetWacRuleOptions<SolidLeaf | SolidContainer>,
): Promise<GetWacRuleResult<SolidLeaf | SolidContainer>> {
const fetch = guaranteeFetch(options?.fetch);
const response = await fetch(aclUri);
Expand All @@ -73,6 +76,13 @@ export async function getWacRuleWithAclUri(
"http://www.w3.org/ns/auth/acl#Authorization",
);

const explicitAuthorizations = authorizations.filter(
(a) => a.accessTo?.["@id"] === resource.uri,
);
const inheritableAuthorizations = authorizations.filter(
(a) => a.default?.["@id"] === resource.uri,
);

const wacRule: WacRule = {
public: {
read: false,
Expand All @@ -98,7 +108,11 @@ export async function getWacRuleWithAclUri(
});
}

authorizations.forEach((authorization) => {
const effectiveAuthorizations = options?.inheritable
? inheritableAuthorizations
: explicitAuthorizations;

effectiveAuthorizations.forEach((authorization) => {
if (
authorization.agentClass?.some(
(agentClass) => agentClass["@id"] === "Agent",
Expand Down
110 changes: 107 additions & 3 deletions packages/connected-solid/test/Integration.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,7 @@ import { getStorageFromWebId } from "../src/getStorageFromWebId";
import type { ResourceInfo } from "@ldo/test-solid-server";
import { createApp, setupServer } from "@ldo/test-solid-server";
import { describe, it, expect, vi, beforeEach, afterEach } from "vitest";
import assert from "node:assert/strict";

const ROOT_CONTAINER = "http://localhost:3001/";
const WEB_ID = "http://localhost:3001/example/profile/card#me";
Expand Down Expand Up @@ -111,6 +112,34 @@ const SAMPLE_PROFILE_TTL = `
<${SAMPLE_PROFILE_URI}> pim:storage <https://example.com/A/>, <https://example.com/B/> .
`;

const OTHER_CONTAINER_SLUG = "other_container/";
const OTHER_CONTAINER_URI =
`${TEST_CONTAINER_URI}${OTHER_CONTAINER_SLUG}` as SolidContainerUri;
const OTHER_CONTAINER_ACL = `
@prefix acl: <http://www.w3.org/ns/auth/acl#>.
@prefix foaf: <http://xmlns.com/foaf/0.1/>.

<#owner> a acl:Authorization;
acl:accessTo <./>;
acl:default <./>;
acl:agent <${WEB_ID}>;
acl:mode acl:Read, acl:Write, acl:Append, acl:Control.

<#public> a acl:Authorization;
acl:accessTo <./>;
acl:agentClass foaf:Agent;
acl:mode acl:Read, acl:Write.

<#public-inherited> a acl:Authorization;
acl:default <./>;
acl:agentClass foaf:Agent;
acl:mode acl:Read, acl:Control.
`;

const OTHER_RESOURCE_SLUG = "resource.ttl";
const OTHER_RESOURCE_URI =
`${OTHER_CONTAINER_URI}${OTHER_RESOURCE_SLUG}` as SolidLeafUri;

const resourceInfo: ResourceInfo = {
slug: TEST_CONTAINER_SLUG,
isContainer: true,
Expand Down Expand Up @@ -159,6 +188,24 @@ const resourceInfo: ResourceInfo = {
mimeType: "text/plain",
data: "",
},
{
slug: OTHER_CONTAINER_SLUG,
isContainer: true,
contains: [
{
slug: ".acl",
isContainer: false,
mimeType: "text/turtle",
data: OTHER_CONTAINER_ACL,
},
{
slug: OTHER_RESOURCE_SLUG,
isContainer: false,
mimeType: "text/turtle",
data: "<#this> a <#Test>.",
},
],
},
],
};

Expand Down Expand Up @@ -281,7 +328,7 @@ describe("Integration", () => {
isDoingInitialFetch: true,
});
expect(result.type).toBe("containerReadSuccess");
expect(resource.children().length).toBe(3);
expect(resource.children().length).toBe(4);
});

it("Reads a binary leaf", async () => {
Expand Down Expand Up @@ -495,7 +542,7 @@ describe("Integration", () => {
},
);
expect(result.type).toBe("containerReadSuccess");
expect(resource.children().length).toBe(3);
expect(resource.children().length).toBe(4);
});

it("reads an unfetched leaf", async () => {
Expand Down Expand Up @@ -526,7 +573,7 @@ describe("Integration", () => {
const result = await resource.readIfUnfetched();
expect(s.fetchMock).not.toHaveBeenCalled();
expect(result.type).toBe("containerReadSuccess");
expect(resource.children().length).toBe(3);
expect(resource.children().length).toBe(4);
});

it("returns a cached existing data leaf", async () => {
Expand Down Expand Up @@ -1850,6 +1897,63 @@ describe("Integration", () => {
});
});

[true, false].forEach((ignoreCache) => {
it(`[${
ignoreCache ? "not cached" : "cached"
}] Handle rule inheritance correctly, distinguish acl:accessTo (direct) and acl:default (inherited)`, async () => {
const container = solidLdoDataset.getResource(OTHER_CONTAINER_URI);

const containerWacResult = await container.getWac({ ignoreCache });
expect(containerWacResult.isError).toBe(false);
// node:assert narrows types
assert.ok(!containerWacResult.isError);

expect(containerWacResult.wacRule.agent[WEB_ID]).toEqual({
read: true,
write: true,
append: true,
control: true,
});

expect(containerWacResult.wacRule.public).toEqual({
read: true,
write: true,
append: false,
control: false,
});

const resource = solidLdoDataset.getResource(OTHER_RESOURCE_URI);
const result = await resource.read();
assert.ok(!result.isError);

const resourceWacResult = await resource.getWac({ ignoreCache });
assert.ok(!resourceWacResult.isError);

expect(resourceWacResult.wacRule.agent[WEB_ID]).toEqual({
read: true,
write: true,
append: true,
control: true,
});

expect(resourceWacResult.wacRule.public).toEqual({
read: true,
write: false,
append: false,
control: true,
});

const finalContainerWacResult = await container.getWac({ ignoreCache });
assert.ok(!finalContainerWacResult.isError);
expect(finalContainerWacResult.wacRule.public).toEqual({
read: true,
write: true,
append: false,
control: false,
});
});
});

it("uses cached values for a retrieved resource", async () => {
const resource = solidLdoDataset.getResource(SAMPLE_DATA_URI);
await resource.getWac();
Expand Down
Loading