Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: check writer permission before merging #303

Open
wants to merge 11 commits into
base: main
Choose a base branch
from
10 changes: 2 additions & 8 deletions packages/blueprints/src/AddWinsSetWithACL/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,21 +23,15 @@ export class AddWinsSetWithACL<T> implements DRP {
if (!this.state.get(value)) this.state.set(value, true);
}

add(sender: string, value: T): void {
if (this.acl && !this.acl.isWriter(sender)) {
throw new Error("Only writers can add values.");
}
add(value: T): void {
this._add(value);
}

private _remove(value: T): void {
if (this.state.get(value)) this.state.set(value, false);
}

remove(sender: string, value: T): void {
if (this.acl && !this.acl.isWriter(sender)) {
throw new Error("Only writers can remove values.");
}
remove(value: T): void {
this._remove(value);
}

Expand Down
50 changes: 42 additions & 8 deletions packages/object/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -164,6 +164,11 @@ export class DRPObject implements IDRPObject {
}

try {
const drp = this._computeDRP(vertex.dependencies);
if (!this._checkWriterPermission(drp, vertex.peerId)) {
throw new Error(`${vertex.peerId} does not have write permission.`);
}

this.hashGraph.addVertex(
vertex.operation,
vertex.dependencies,
Expand All @@ -172,7 +177,8 @@ export class DRPObject implements IDRPObject {
vertex.signature,
);

this._setState(vertex);
this._applyOperation(drp, vertex.operation);
this._setState(vertex, this._getDRPState(drp));
} catch (e) {
missing.push(vertex.hash);
}
Expand All @@ -196,6 +202,13 @@ export class DRPObject implements IDRPObject {
}
}

private _checkWriterPermission(drp: DRP, peerId: string): boolean {
if (drp.acl) {
return drp.acl.isWriter(peerId);
}
return true;
}

private _applyOperation(drp: DRP, operation: Operation) {
const { type, value } = operation;

Expand All @@ -218,11 +231,10 @@ export class DRPObject implements IDRPObject {
target[methodName](...args);
}

private _computeState(
private _computeDRP(
vertexDependencies: Hash[],
vertexOperation?: Operation | undefined,
// biome-ignore lint: values can be anything
): Map<string, any> {
): DRP {
const subgraph: ObjectSet<Hash> = new ObjectSet();
const lca =
vertexDependencies.length === 1
Expand Down Expand Up @@ -256,6 +268,13 @@ export class DRPObject implements IDRPObject {
this._applyOperation(drp, vertexOperation);
}

return drp;
}

private _getDRPState(
drp: DRP,
// biome-ignore lint: values can be anything
): Map<string, any> {
const varNames: string[] = Object.keys(drp);
// biome-ignore lint: values can be anything
const newState: Map<string, any> = new Map();
Expand All @@ -265,17 +284,32 @@ export class DRPObject implements IDRPObject {
return newState;
}

private _setState(vertex: Vertex) {
const newState = this._computeState(vertex.dependencies, vertex.operation);
this.states.set(vertex.hash, { state: newState });
private _computeDRPState(
vertexDependencies: Hash[],
vertexOperation?: Operation | undefined,
// biome-ignore lint: values can be anything
): Map<string, any> {
const drp = this._computeDRP(vertexDependencies, vertexOperation);
return this._getDRPState(drp);
}

private _setState(
vertex: Vertex,
// biome-ignore lint: values can be anything
state?: Map<string, any>,
) {
this.states.set(vertex.hash, {
state:
state ?? this._computeDRPState(vertex.dependencies, vertex.operation),
});
}

private _updateDRPState() {
if (!this.drp) {
return;
}
const currentDRP = this.drp as DRP;
const newState = this._computeState(this.hashGraph.getFrontier());
const newState = this._computeDRPState(this.hashGraph.getFrontier());
for (const [key, value] of newState.entries()) {
if (key in currentDRP && typeof currentDRP[key] !== "function") {
currentDRP[key] = value;
Expand Down
96 changes: 94 additions & 2 deletions packages/object/tests/hashgraph.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -675,7 +675,7 @@ describe("Operation with ACL tests", () => {
drp1.acl.grant("peer1", "peer2", "publicKey2");
obj2.merge(obj1.hashGraph.getAllVertices());

drp2.add("peer2", 1);
drp2.add(1);
obj1.merge(obj2.hashGraph.getAllVertices());
expect(drp1.contains(1)).toBe(true);
});
Expand All @@ -691,11 +691,103 @@ describe("Operation with ACL tests", () => {
obj2.merge(obj1.hashGraph.getAllVertices());

expect(drp2.acl.isWriter("peer2")).toBe(true);
drp2.add("peer2", 1);
drp2.add(1);

obj1.merge(obj2.hashGraph.getAllVertices());
drp1.acl.revoke("peer1", "peer2");
obj2.merge(obj1.hashGraph.getAllVertices());
expect(drp2.acl.isWriter("peer2")).toBe(false);
});
});

describe("Writer permission tests", () => {
let obj1: DRPObject;
let obj2: DRPObject;
let obj3: DRPObject;

beforeEach(async () => {
const peerIdToPublicKeyMap = new Map([["peer1", "publicKey1"]]);
obj1 = new DRPObject("peer1", new AddWinsSetWithACL(peerIdToPublicKeyMap));
obj2 = new DRPObject("peer2", new AddWinsSetWithACL(peerIdToPublicKeyMap));
obj3 = new DRPObject("peer3", new AddWinsSetWithACL(peerIdToPublicKeyMap));
});

test("Node without writer permission can generate vertex locally", () => {
const drp = obj1.drp as AddWinsSetWithACL<number>;
drp.add(1);
drp.add(2);

expect(drp.contains(1)).toBe(true);
expect(drp.contains(2)).toBe(true);
});

test("Discard vertex if creator does not have write permission", () => {
const drp1 = obj1.drp as AddWinsSetWithACL<number>;
const drp2 = obj2.drp as AddWinsSetWithACL<number>;

drp1.add(1);
drp2.add(2);

obj1.merge(obj2.hashGraph.getAllVertices());
expect(drp1.contains(2)).toBe(false);
});

test("Accept vertex if creator has write permission", () => {
/*
ROOT -- V1:ADD(1) -- V2:GRANT(peer2) -- V3:ADD(4)
*/
const drp1 = obj1.drp as AddWinsSetWithACL<number>;
const drp2 = obj2.drp as AddWinsSetWithACL<number>;

drp1.add(1);
drp1.acl.grant("peer1", "peer2", "publicKey2");
expect(drp1.acl.isAdmin("peer1")).toBe(true);

obj2.merge(obj1.hashGraph.getAllVertices());
expect(drp2.contains(1)).toBe(true);
expect(drp2.acl.isWriter("peer2")).toBe(true);

drp2.add(4);
obj1.merge(obj2.hashGraph.getAllVertices());
expect(drp1.contains(4)).toBe(true);
});

test("Discard vertex if writer permission is revoked", () => {
/*
__ V4:ADD(1) --
/ \
ROOT -- V1:GRANT(peer2) -- V2:grant(peer3) V6:REVOKE(peer3) -- V7:ADD(4)
\ /
-- V5:ADD(2) --
*/
const drp1 = obj1.drp as AddWinsSetWithACL<number>;
const drp2 = obj2.drp as AddWinsSetWithACL<number>;
const drp3 = obj3.drp as AddWinsSetWithACL<number>;

drp1.acl.grant("peer1", "peer2", "publicKey2");
drp1.acl.grant("peer1", "peer3", "publicKey3");
obj2.merge(obj1.hashGraph.getAllVertices());
obj3.merge(obj1.hashGraph.getAllVertices());

drp2.add(1);
drp3.add(2);
obj1.merge(obj2.hashGraph.getAllVertices());
obj1.merge(obj3.hashGraph.getAllVertices());
obj2.merge(obj3.hashGraph.getAllVertices());
obj3.merge(obj2.hashGraph.getAllVertices());
expect(drp1.contains(1)).toBe(true);
expect(drp1.contains(2)).toBe(true);

drp1.acl.revoke("peer1", "peer3");
obj3.merge(obj1.hashGraph.getAllVertices());
drp3.add(3);
obj2.merge(obj3.hashGraph.getAllVertices());
expect(drp2.contains(3)).toBe(false);

drp2.add(4);
obj1.merge(obj2.hashGraph.getAllVertices());
obj1.merge(obj3.hashGraph.getAllVertices());
expect(drp1.contains(3)).toBe(false);
expect(drp1.contains(4)).toBe(true);
});
});
Loading