Completed setWac

main
jaxoncreed 2 years ago
parent 59013086eb
commit 087f57b4f5
  1. 15
      packages/solid/src/resource/Resource.ts
  2. 7
      packages/solid/src/resource/wac/WacRule.ts
  3. 3
      packages/solid/src/resource/wac/getWacUri.ts
  4. 3
      packages/solid/src/resource/wac/results/GetWacUriSuccess.ts
  5. 100
      packages/solid/src/resource/wac/setWacRule.ts
  6. 115
      packages/solid/test/Integration.test.ts

@ -30,6 +30,8 @@ import type { GetWacUriError, GetWacUriResult } from "./wac/getWacUri";
import { getWacUri } from "./wac/getWacUri"; import { getWacUri } from "./wac/getWacUri";
import { getWacRuleWithAclUri, type GetWacRuleResult } from "./wac/getWacRule"; import { getWacRuleWithAclUri, type GetWacRuleResult } from "./wac/getWacRule";
import { NoncompliantPodError } from "../requester/results/error/NoncompliantPodError"; import { NoncompliantPodError } from "../requester/results/error/NoncompliantPodError";
import { setWacRuleForAclUri, type SetWacRuleResult } from "./wac/setWacRule";
import type { LeafUri } from "../util/uriTypes";
/** /**
* Statuses shared between both Leaf and Container * Statuses shared between both Leaf and Container
@ -85,7 +87,7 @@ export abstract class Resource extends (EventEmitter as new () => TypedEmitter<{
* @internal * @internal
* If a wac uri is fetched, it is cached here * If a wac uri is fetched, it is cached here
*/ */
protected wacUri?: string; protected wacUri?: LeafUri;
/** /**
* @internal * @internal
@ -597,7 +599,12 @@ export abstract class Resource extends (EventEmitter as new () => TypedEmitter<{
return parentResource.getWac(); return parentResource.getWac();
} }
// async setWac(wacRule: WacRule): Promise<> { async setWac(wacRule: WacRule): Promise<GetWacUriError | SetWacRuleResult> {
// throw new Error("Not Implemented"); const wacUriResult = await this.getWacUri();
// } if (wacUriResult.isError) return wacUriResult;
return setWacRuleForAclUri(wacUriResult.wacUri, wacRule, this.uri, {
fetch: this.context.fetch,
});
}
} }

@ -10,10 +10,3 @@ export interface WacRule {
authenticated: AccessModeList; authenticated: AccessModeList;
agent: Record<string, AccessModeList>; agent: Record<string, AccessModeList>;
} }
// export interface SetWacRule {
// ruleFor: Resource;
// public?: AccessModeList;
// authenticated?: AccessModeList;
// agent?: Record<string, AccessModeList>;
// }

@ -9,6 +9,7 @@ import { guaranteeFetch } from "../../util/guaranteeFetch";
import type { BasicRequestOptions } from "../../requester/requests/requestOptions"; import type { BasicRequestOptions } from "../../requester/requests/requestOptions";
import { NoncompliantPodError } from "../../requester/results/error/NoncompliantPodError"; import { NoncompliantPodError } from "../../requester/results/error/NoncompliantPodError";
import { parse as parseLinkHeader } from "http-link-header"; import { parse as parseLinkHeader } from "http-link-header";
import type { LeafUri } from "../../util/uriTypes";
export type GetWacUriError = export type GetWacUriError =
| HttpErrorResultType | HttpErrorResultType
@ -55,7 +56,7 @@ export async function getWacUri(
type: "getWacUriSuccess", type: "getWacUriSuccess",
isError: false, isError: false,
uri: resourceUri, uri: resourceUri,
wacUri: aclUris[0].uri, wacUri: aclUris[0].uri as LeafUri,
}; };
} catch (err: unknown) { } catch (err: unknown) {
return UnexpectedResourceError.fromThrown(resourceUri, err); return UnexpectedResourceError.fromThrown(resourceUri, err);

@ -1,6 +1,7 @@
import type { ResourceSuccess } from "../../../requester/results/success/SuccessResult"; import type { ResourceSuccess } from "../../../requester/results/success/SuccessResult";
import type { LeafUri } from "../../../util/uriTypes";
export interface GetWacUriSuccess extends ResourceSuccess { export interface GetWacUriSuccess extends ResourceSuccess {
type: "getWacUriSuccess"; type: "getWacUriSuccess";
wacUri: string; wacUri: LeafUri;
} }

@ -0,0 +1,100 @@
import { createLdoDataset } from "@ldo/ldo";
import type { BasicRequestOptions } from "../../requester/requests/requestOptions";
import type { UnexpectedResourceError } from "../../requester/results/error/ErrorResult";
import {
HttpErrorResult,
type HttpErrorResultType,
} from "../../requester/results/error/HttpErrorResult";
import { isContainerUri, type LeafUri } from "../../util/uriTypes";
import type { AccessModeList, WacRule } from "./WacRule";
import type { SetWacRuleSuccess } from "./results/SetWacRuleSuccess";
import type { Authorization } from "../../.ldo/wac.typings";
import { AuthorizationShapeType } from "../../.ldo/wac.shapeTypes";
import { v4 } from "uuid";
import { guaranteeFetch } from "../../util/guaranteeFetch";
export type SetWacRuleError = HttpErrorResultType | UnexpectedResourceError;
export type SetWacRuleResult = SetWacRuleSuccess | SetWacRuleError;
export async function setWacRuleForAclUri(
aclUri: LeafUri,
newRule: WacRule,
accessTo: string,
options?: BasicRequestOptions,
): Promise<SetWacRuleResult> {
const fetch = guaranteeFetch(options?.fetch);
// The rule map keeps track of all the rules that are currently being used
// so that similar rules can be grouped together
const ruleMap: Record<string, Authorization> = {};
// The dataset that will eventually be sent to the Pod
const dataset = createLdoDataset();
// Helper function to add rules to the dataset by grouping them in the ruleMap
function addRuleToDataset(
type: "public" | "authenticated" | "agent",
accessModeList: AccessModeList,
agentId?: string,
) {
const accessModeListHash = hashAccessModeList(accessModeList);
// No need to add if all access is false
if (accessModeListHash === "") return;
if (!ruleMap[accessModeListHash]) {
const authorization = dataset
.usingType(AuthorizationShapeType)
.fromSubject(`${aclUri}#${v4()}`);
authorization.type = { "@id": "Authorization" };
if (accessModeList.read) authorization.mode?.push({ "@id": "Read" });
if (accessModeList.write) authorization.mode?.push({ "@id": "Write" });
if (accessModeList.append) authorization.mode?.push({ "@id": "Append" });
if (accessModeList.control)
authorization.mode?.push({ "@id": "Control" });
authorization.accessTo = { "@id": accessTo };
if (isContainerUri(accessTo)) {
authorization.default = { "@id": accessTo };
}
ruleMap[accessModeListHash] = authorization;
}
const authorization = ruleMap[accessModeListHash];
// Add agents to the rule
if (type === "public") {
authorization.agentClass?.push({ "@id": "Agent" });
} else if (type === "authenticated") {
authorization.agentClass?.push({ "@id": "AuthenticatedAgent" });
} else if (type === "agent" && agentId) {
authorization.agent?.push({ "@id": agentId });
}
}
// Add each rule to the dataset
addRuleToDataset("public", newRule.public);
addRuleToDataset("authenticated", newRule.authenticated);
Object.entries(newRule.agent).forEach(([agentUri, accessModeList]) => {
addRuleToDataset("agent", accessModeList, agentUri);
});
// Save to Pod
const response = await fetch(aclUri, {
method: "PUT",
headers: {
"content-type": "text/turtle",
},
body: dataset.toString(),
});
const errorResult = HttpErrorResult.checkResponse(aclUri, response);
if (errorResult) return errorResult;
return {
type: "setWacRuleSuccess",
uri: aclUri,
isError: false,
wacRule: newRule,
};
}
// Hashes the access mode list for use in the rule map
function hashAccessModeList(list: AccessModeList): string {
return Object.entries(list).reduce(
(agg, [key, isPresent]) => (isPresent ? agg + key : agg),
"",
);
}

@ -44,6 +44,7 @@ import type {
} from "../src/requester/results/error/HttpErrorResult"; } from "../src/requester/results/error/HttpErrorResult";
import type { NoncompliantPodError } from "../src/requester/results/error/NoncompliantPodError"; import type { NoncompliantPodError } from "../src/requester/results/error/NoncompliantPodError";
import type { GetWacRuleSuccess } from "../src/resource/wac/results/GetWacRuleSuccess"; import type { GetWacRuleSuccess } from "../src/resource/wac/results/GetWacRuleSuccess";
import type { WacRule } from "../src/resource/wac/WacRule";
const TEST_CONTAINER_SLUG = "test_ldo/"; const TEST_CONTAINER_SLUG = "test_ldo/";
const TEST_CONTAINER_URI = const TEST_CONTAINER_URI =
@ -211,6 +212,9 @@ describe("Integration", () => {
method: "DELETE", method: "DELETE",
}), }),
]); ]);
await authFetch(TEST_CONTAINER_URI, {
method: "DELETE",
});
}); });
/** /**
@ -1735,7 +1739,7 @@ describe("Integration", () => {
expect(wacResult.isError).toBe(true); expect(wacResult.isError).toBe(true);
expect(wacResult.type).toBe("noncompliantPodError"); expect(wacResult.type).toBe("noncompliantPodError");
expect((wacResult as NoncompliantPodError).message).toBe( expect((wacResult as NoncompliantPodError).message).toBe(
"Response from http://localhost:3001/test_ldo/sample.ttl is not compliant with the Solid Specification: No link header present in request.", `Response from ${SAMPLE_DATA_URI} is not compliant with the Solid Specification: No link header present in request.`,
); );
}); });
@ -1751,7 +1755,7 @@ describe("Integration", () => {
expect(wacResult.isError).toBe(true); expect(wacResult.isError).toBe(true);
expect(wacResult.type).toBe("noncompliantPodError"); expect(wacResult.type).toBe("noncompliantPodError");
expect((wacResult as NoncompliantPodError).message).toBe( expect((wacResult as NoncompliantPodError).message).toBe(
`Response from http://localhost:3001/test_ldo/sample.ttl is not compliant with the Solid Specification: There must be one link with a rel="acl"`, `Response from ${SAMPLE_DATA_URI} is not compliant with the Solid Specification: There must be one link with a rel="acl"`,
); );
}); });
@ -1814,22 +1818,99 @@ describe("Integration", () => {
expect(wacResult.isError).toBe(true); expect(wacResult.isError).toBe(true);
expect(wacResult.type).toBe("serverError"); expect(wacResult.type).toBe("serverError");
}); });
it("returns a NonCompliantPodError when this is the root resource and it doesn't have an ACL", async () => {
const resource = solidLdoDataset.getResource(ROOT_CONTAINER);
fetchMock.mockResolvedValueOnce(
new Response("", {
status: 200,
headers: { link: `<card.acl>; rel="acl"` },
}),
);
fetchMock.mockResolvedValueOnce(new Response("", { status: 404 }));
const wacResult = await resource.getWac();
expect(wacResult.isError).toBe(true);
expect(wacResult.type).toBe("noncompliantPodError");
expect((wacResult as NoncompliantPodError).message).toBe(
`Response from ${ROOT_CONTAINER} is not compliant with the Solid Specification: Resource "${ROOT_CONTAINER}" has no Effective ACL resource`,
);
});
}); });
it("returns a NonCompliantPodError when this is the root resource and it doesn't have an ACL", async () => { describe("setWacRule", () => {
const resource = solidLdoDataset.getResource(ROOT_CONTAINER); const newRules: WacRule = {
fetchMock.mockResolvedValueOnce( public: { read: true, write: false, append: false, control: false },
new Response("", { authenticated: {
status: 200, read: true,
headers: { link: `<card.acl>; rel="acl"` }, write: false,
}), append: true,
); control: false,
fetchMock.mockResolvedValueOnce(new Response("", { status: 404 })); },
const wacResult = await resource.getWac(); agent: {
expect(wacResult.isError).toBe(true); [WEB_ID]: { read: true, write: true, append: true, control: true },
expect(wacResult.type).toBe("noncompliantPodError"); },
expect((wacResult as NoncompliantPodError).message).toBe( };
`Response from http://localhost:3001/ is not compliant with the Solid Specification: Resource "${ROOT_CONTAINER}" has no Effective ACL resource`,
); it("sets wac rules for a resource that didn't have one before", async () => {
const resource = solidLdoDataset.getResource(SAMPLE_DATA_URI);
const result = await resource.setWac(newRules);
expect(result.isError).toBe(false);
expect(result.type).toBe("setWacRuleSuccess");
const readResult = await resource.getWac({ ignoreCache: true });
expect(readResult.isError).toBe(false);
expect(readResult.type).toBe("getWacRuleSuccess");
const rules = (readResult as GetWacRuleSuccess).wacRule;
expect(rules).toEqual(newRules);
});
it("overwrites an existing access control rule", async () => {
const resource = solidLdoDataset.getResource(TEST_CONTAINER_URI);
const result = await resource.setWac(newRules);
expect(result.isError).toBe(false);
expect(result.type).toBe("setWacRuleSuccess");
const readResult = await resource.getWac({ ignoreCache: true });
expect(readResult.isError).toBe(false);
expect(readResult.type).toBe("getWacRuleSuccess");
const rules = (readResult as GetWacRuleSuccess).wacRule;
expect(rules).toEqual(newRules);
});
it("Does not write a rule when access is not granted to an agent", async () => {
const moreRules = {
...newRules,
public: { read: false, write: false, append: false, control: false },
};
const resource = solidLdoDataset.getResource(SAMPLE_DATA_URI);
const result = await resource.setWac(moreRules);
expect(result.isError).toBe(false);
expect(result.type).toBe("setWacRuleSuccess");
const readResult = await resource.getWac({ ignoreCache: true });
expect(readResult.isError).toBe(false);
expect(readResult.type).toBe("getWacRuleSuccess");
const rules = (readResult as GetWacRuleSuccess).wacRule;
expect(rules).toEqual(moreRules);
});
it("returns an error when an error is encountered fetching the aclUri", async () => {
const resource = solidLdoDataset.getResource(SAMPLE_DATA_URI);
fetchMock.mockResolvedValueOnce(new Response("Error", { status: 500 }));
const wacResult = await resource.setWac(newRules);
expect(wacResult.isError).toBe(true);
expect(wacResult.type).toBe("serverError");
});
it("Returns an error when the request to write the access rules throws an error", async () => {
const resource = solidLdoDataset.getResource(TEST_CONTAINER_URI);
fetchMock.mockResolvedValueOnce(
new Response("", {
status: 200,
headers: { link: `<card.acl>; rel="acl"` },
}),
);
fetchMock.mockResolvedValueOnce(new Response("", { status: 500 }));
const wacResult = await resource.setWac(newRules);
expect(wacResult.isError).toBe(true);
expect(wacResult.type).toBe("serverError");
});
}); });
}); });

Loading…
Cancel
Save