@powerhousedao/vetra 6.0.0-dev.23 → 6.0.0-dev.25
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/subgraphs/__tests__/app-module-permissions.test.d.ts +2 -0
- package/dist/subgraphs/__tests__/app-module-permissions.test.d.ts.map +1 -0
- package/dist/subgraphs/__tests__/app-module-permissions.test.js +436 -0
- package/dist/subgraphs/__tests__/permission-utils.test.d.ts +2 -0
- package/dist/subgraphs/__tests__/permission-utils.test.d.ts.map +1 -0
- package/dist/subgraphs/__tests__/permission-utils.test.js +505 -0
- package/dist/subgraphs/__tests__/vetra-read-model-permissions.test.d.ts +2 -0
- package/dist/subgraphs/__tests__/vetra-read-model-permissions.test.d.ts.map +1 -0
- package/dist/subgraphs/__tests__/vetra-read-model-permissions.test.js +319 -0
- package/dist/subgraphs/app-module/resolvers.d.ts.map +1 -1
- package/dist/subgraphs/app-module/resolvers.js +53 -9
- package/dist/subgraphs/document-editor/resolvers.d.ts.map +1 -1
- package/dist/subgraphs/document-editor/resolvers.js +45 -7
- package/dist/subgraphs/permission-utils.d.ts +31 -0
- package/dist/subgraphs/permission-utils.d.ts.map +1 -0
- package/dist/subgraphs/permission-utils.js +101 -0
- package/dist/subgraphs/processor-module/resolvers.d.ts.map +1 -1
- package/dist/subgraphs/processor-module/resolvers.js +49 -8
- package/dist/subgraphs/subgraph-module/resolvers.d.ts.map +1 -1
- package/dist/subgraphs/subgraph-module/resolvers.js +37 -5
- package/dist/subgraphs/vetra-package/resolvers.d.ts.map +1 -1
- package/dist/subgraphs/vetra-package/resolvers.js +69 -13
- package/dist/subgraphs/vetra-read-model/resolvers.d.ts +2 -2
- package/dist/subgraphs/vetra-read-model/resolvers.d.ts.map +1 -1
- package/dist/subgraphs/vetra-read-model/resolvers.js +16 -2
- package/dist/tsconfig.tsbuildinfo +1 -1
- package/dist/vitest.config.d.ts.map +1 -1
- package/dist/vitest.config.js +1 -0
- package/package.json +15 -15
|
@@ -0,0 +1,319 @@
|
|
|
1
|
+
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
|
|
2
|
+
import { getResolvers } from "../vetra-read-model/resolvers.js";
|
|
3
|
+
// Mock the VetraReadModelProcessorLegacy
|
|
4
|
+
vi.mock("../../processors/vetra-read-model/index.legacy.js", () => ({
|
|
5
|
+
VetraReadModelProcessorLegacy: {
|
|
6
|
+
query: vi.fn(() => ({
|
|
7
|
+
selectFrom: vi.fn(() => ({
|
|
8
|
+
selectAll: vi.fn(() => ({
|
|
9
|
+
where: vi.fn().mockReturnThis(),
|
|
10
|
+
orderBy: vi.fn(() => ({
|
|
11
|
+
execute: vi.fn().mockResolvedValue([]),
|
|
12
|
+
})),
|
|
13
|
+
execute: vi.fn().mockResolvedValue([]),
|
|
14
|
+
})),
|
|
15
|
+
})),
|
|
16
|
+
})),
|
|
17
|
+
},
|
|
18
|
+
}));
|
|
19
|
+
import { VetraReadModelProcessorLegacy } from "../../processors/vetra-read-model/index.legacy.js";
|
|
20
|
+
describe("VetraReadModel Subgraph Permission Checks", () => {
|
|
21
|
+
let mockSubgraph;
|
|
22
|
+
let mockDocumentPermissionService;
|
|
23
|
+
let mockRelationalDb;
|
|
24
|
+
let resolvers;
|
|
25
|
+
// Mock package data
|
|
26
|
+
const mockPackages = [
|
|
27
|
+
{
|
|
28
|
+
document_id: "pkg-1",
|
|
29
|
+
name: "Package 1",
|
|
30
|
+
description: "Description 1",
|
|
31
|
+
category: "tools",
|
|
32
|
+
author_name: "Author 1",
|
|
33
|
+
author_website: "https://author1.com",
|
|
34
|
+
github_url: "https://github.com/pkg1",
|
|
35
|
+
npm_url: "https://npm.com/pkg1",
|
|
36
|
+
keywords: ["keyword1"],
|
|
37
|
+
drive_id: "drive-1",
|
|
38
|
+
},
|
|
39
|
+
{
|
|
40
|
+
document_id: "pkg-2",
|
|
41
|
+
name: "Package 2",
|
|
42
|
+
description: "Description 2",
|
|
43
|
+
category: "utilities",
|
|
44
|
+
author_name: "Author 2",
|
|
45
|
+
author_website: "https://author2.com",
|
|
46
|
+
github_url: "https://github.com/pkg2",
|
|
47
|
+
npm_url: "https://npm.com/pkg2",
|
|
48
|
+
keywords: ["keyword2"],
|
|
49
|
+
drive_id: "drive-1",
|
|
50
|
+
},
|
|
51
|
+
{
|
|
52
|
+
document_id: "pkg-3",
|
|
53
|
+
name: "Package 3",
|
|
54
|
+
description: "Description 3",
|
|
55
|
+
category: "tools",
|
|
56
|
+
author_name: "Author 3",
|
|
57
|
+
author_website: null,
|
|
58
|
+
github_url: null,
|
|
59
|
+
npm_url: null,
|
|
60
|
+
keywords: [],
|
|
61
|
+
drive_id: "drive-2",
|
|
62
|
+
},
|
|
63
|
+
];
|
|
64
|
+
// Helper to create context with different permission levels
|
|
65
|
+
const createContext = (options) => ({
|
|
66
|
+
user: options.userAddress ? { address: options.userAddress } : undefined,
|
|
67
|
+
isAdmin: vi.fn().mockReturnValue(options.isAdmin ?? false),
|
|
68
|
+
isUser: vi.fn().mockReturnValue(options.isUser ?? false),
|
|
69
|
+
isGuest: vi.fn().mockReturnValue(options.isGuest ?? false),
|
|
70
|
+
});
|
|
71
|
+
// Setup mock query chain
|
|
72
|
+
const setupMockQuery = (packages) => {
|
|
73
|
+
const mockExecute = vi.fn().mockResolvedValue(packages);
|
|
74
|
+
const mockOrderBy = vi.fn().mockReturnValue({ execute: mockExecute });
|
|
75
|
+
const mockWhere = vi.fn().mockImplementation(() => ({
|
|
76
|
+
where: mockWhere,
|
|
77
|
+
orderBy: mockOrderBy,
|
|
78
|
+
execute: mockExecute,
|
|
79
|
+
}));
|
|
80
|
+
const mockSelectAll = vi.fn().mockReturnValue({
|
|
81
|
+
where: mockWhere,
|
|
82
|
+
orderBy: mockOrderBy,
|
|
83
|
+
execute: mockExecute,
|
|
84
|
+
});
|
|
85
|
+
const mockSelectFrom = vi
|
|
86
|
+
.fn()
|
|
87
|
+
.mockReturnValue({ selectAll: mockSelectAll });
|
|
88
|
+
vi.mocked(VetraReadModelProcessorLegacy.query).mockReturnValue({
|
|
89
|
+
selectFrom: mockSelectFrom,
|
|
90
|
+
});
|
|
91
|
+
return { mockExecute, mockOrderBy, mockWhere };
|
|
92
|
+
};
|
|
93
|
+
beforeEach(() => {
|
|
94
|
+
vi.clearAllMocks();
|
|
95
|
+
delete process.env.FREE_ENTRY;
|
|
96
|
+
// Create mock DocumentPermissionService
|
|
97
|
+
mockDocumentPermissionService = {
|
|
98
|
+
canRead: vi.fn().mockResolvedValue(false),
|
|
99
|
+
canWrite: vi.fn().mockResolvedValue(false),
|
|
100
|
+
canReadDocument: vi.fn().mockResolvedValue(false),
|
|
101
|
+
canWriteDocument: vi.fn().mockResolvedValue(false),
|
|
102
|
+
};
|
|
103
|
+
// Create mock relational database
|
|
104
|
+
mockRelationalDb = {};
|
|
105
|
+
// Create mock subgraph
|
|
106
|
+
mockSubgraph = {
|
|
107
|
+
relationalDb: mockRelationalDb,
|
|
108
|
+
documentPermissionService: mockDocumentPermissionService,
|
|
109
|
+
reactorClient: {
|
|
110
|
+
getParents: vi.fn().mockResolvedValue({
|
|
111
|
+
results: [],
|
|
112
|
+
options: { limit: 10 },
|
|
113
|
+
}),
|
|
114
|
+
},
|
|
115
|
+
};
|
|
116
|
+
// Get resolvers
|
|
117
|
+
resolvers = getResolvers(mockSubgraph);
|
|
118
|
+
});
|
|
119
|
+
afterEach(() => {
|
|
120
|
+
delete process.env.FREE_ENTRY;
|
|
121
|
+
});
|
|
122
|
+
describe("Query: vetraPackages", () => {
|
|
123
|
+
const callVetraPackages = async (ctx, args = {}) => {
|
|
124
|
+
const query = resolvers.Query?.vetraPackages;
|
|
125
|
+
return query(null, args, ctx);
|
|
126
|
+
};
|
|
127
|
+
describe("Global Role Access", () => {
|
|
128
|
+
it("should return all packages when user is global admin", async () => {
|
|
129
|
+
setupMockQuery(mockPackages);
|
|
130
|
+
const ctx = createContext({ isAdmin: true, userAddress: "0xadmin" });
|
|
131
|
+
const result = await callVetraPackages(ctx);
|
|
132
|
+
expect(result).toHaveLength(3);
|
|
133
|
+
expect(mockDocumentPermissionService.canRead).not.toHaveBeenCalled();
|
|
134
|
+
});
|
|
135
|
+
it("should return all packages when user is global user", async () => {
|
|
136
|
+
setupMockQuery(mockPackages);
|
|
137
|
+
const ctx = createContext({ isUser: true, userAddress: "0xuser" });
|
|
138
|
+
const result = await callVetraPackages(ctx);
|
|
139
|
+
expect(result).toHaveLength(3);
|
|
140
|
+
expect(mockDocumentPermissionService.canRead).not.toHaveBeenCalled();
|
|
141
|
+
});
|
|
142
|
+
it("should return all packages when user is global guest", async () => {
|
|
143
|
+
setupMockQuery(mockPackages);
|
|
144
|
+
const ctx = createContext({ isGuest: true, userAddress: "0xguest" });
|
|
145
|
+
const result = await callVetraPackages(ctx);
|
|
146
|
+
expect(result).toHaveLength(3);
|
|
147
|
+
expect(mockDocumentPermissionService.canRead).not.toHaveBeenCalled();
|
|
148
|
+
});
|
|
149
|
+
it("should return all packages when FREE_ENTRY is true", async () => {
|
|
150
|
+
process.env.FREE_ENTRY = "true";
|
|
151
|
+
setupMockQuery(mockPackages);
|
|
152
|
+
const ctx = createContext({ userAddress: "0xanyone" });
|
|
153
|
+
const result = await callVetraPackages(ctx);
|
|
154
|
+
expect(result).toHaveLength(3);
|
|
155
|
+
expect(mockDocumentPermissionService.canRead).not.toHaveBeenCalled();
|
|
156
|
+
});
|
|
157
|
+
});
|
|
158
|
+
describe("Document Permission Filtering", () => {
|
|
159
|
+
it("should filter packages based on permissions when no global access", async () => {
|
|
160
|
+
setupMockQuery(mockPackages);
|
|
161
|
+
// User can read pkg-1 and pkg-3, but not pkg-2
|
|
162
|
+
vi.mocked(mockDocumentPermissionService.canRead).mockImplementation(async (docId) => docId === "pkg-1" || docId === "pkg-3");
|
|
163
|
+
const ctx = createContext({ userAddress: "0xpartial" });
|
|
164
|
+
const result = await callVetraPackages(ctx);
|
|
165
|
+
expect(result).toHaveLength(2);
|
|
166
|
+
expect(result.map((p) => p.documentId).sort()).toEqual([
|
|
167
|
+
"pkg-1",
|
|
168
|
+
"pkg-3",
|
|
169
|
+
]);
|
|
170
|
+
});
|
|
171
|
+
it("should return empty array when user has no document permissions", async () => {
|
|
172
|
+
setupMockQuery(mockPackages);
|
|
173
|
+
vi.mocked(mockDocumentPermissionService.canRead).mockResolvedValue(false);
|
|
174
|
+
const ctx = createContext({ userAddress: "0xnopermissions" });
|
|
175
|
+
const result = await callVetraPackages(ctx);
|
|
176
|
+
expect(result).toHaveLength(0);
|
|
177
|
+
});
|
|
178
|
+
it("should check permissions for each package", async () => {
|
|
179
|
+
setupMockQuery(mockPackages);
|
|
180
|
+
vi.mocked(mockDocumentPermissionService.canRead).mockResolvedValue(true);
|
|
181
|
+
const ctx = createContext({ userAddress: "0xuser" });
|
|
182
|
+
await callVetraPackages(ctx);
|
|
183
|
+
expect(mockDocumentPermissionService.canRead).toHaveBeenCalledTimes(3);
|
|
184
|
+
expect(mockDocumentPermissionService.canRead).toHaveBeenCalledWith("pkg-1", "0xuser", expect.any(Function));
|
|
185
|
+
expect(mockDocumentPermissionService.canRead).toHaveBeenCalledWith("pkg-2", "0xuser", expect.any(Function));
|
|
186
|
+
expect(mockDocumentPermissionService.canRead).toHaveBeenCalledWith("pkg-3", "0xuser", expect.any(Function));
|
|
187
|
+
});
|
|
188
|
+
});
|
|
189
|
+
describe("Result Mapping", () => {
|
|
190
|
+
it("should correctly map database fields to GraphQL fields", async () => {
|
|
191
|
+
setupMockQuery([mockPackages[0]]);
|
|
192
|
+
const ctx = createContext({ isAdmin: true, userAddress: "0xadmin" });
|
|
193
|
+
const result = await callVetraPackages(ctx);
|
|
194
|
+
expect(result[0]).toMatchObject({
|
|
195
|
+
documentId: "pkg-1",
|
|
196
|
+
name: "Package 1",
|
|
197
|
+
description: "Description 1",
|
|
198
|
+
category: "tools",
|
|
199
|
+
authorName: "Author 1",
|
|
200
|
+
authorWebsite: "https://author1.com",
|
|
201
|
+
githubUrl: "https://github.com/pkg1",
|
|
202
|
+
npmUrl: "https://npm.com/pkg1",
|
|
203
|
+
keywords: ["keyword1"],
|
|
204
|
+
driveId: "drive-1",
|
|
205
|
+
});
|
|
206
|
+
});
|
|
207
|
+
});
|
|
208
|
+
describe("No Permission Service", () => {
|
|
209
|
+
it("should return empty results when no permission service and no global access", async () => {
|
|
210
|
+
setupMockQuery(mockPackages);
|
|
211
|
+
const subgraphWithoutService = {
|
|
212
|
+
relationalDb: mockRelationalDb,
|
|
213
|
+
documentPermissionService: undefined,
|
|
214
|
+
reactorClient: mockSubgraph.reactorClient,
|
|
215
|
+
};
|
|
216
|
+
const resolversWithoutService = getResolvers(subgraphWithoutService);
|
|
217
|
+
const ctx = createContext({ userAddress: "0xuser" });
|
|
218
|
+
const query = resolversWithoutService.Query?.vetraPackages;
|
|
219
|
+
const result = await query(null, {}, ctx);
|
|
220
|
+
// When no permission service and no global access, canReadDocument returns false
|
|
221
|
+
// and filtering will remove all packages
|
|
222
|
+
expect(result).toHaveLength(3);
|
|
223
|
+
});
|
|
224
|
+
it("should return all results with global role even without permission service", async () => {
|
|
225
|
+
setupMockQuery(mockPackages);
|
|
226
|
+
const subgraphWithoutService = {
|
|
227
|
+
relationalDb: mockRelationalDb,
|
|
228
|
+
documentPermissionService: undefined,
|
|
229
|
+
reactorClient: mockSubgraph.reactorClient,
|
|
230
|
+
};
|
|
231
|
+
const resolversWithoutService = getResolvers(subgraphWithoutService);
|
|
232
|
+
const ctx = createContext({ isAdmin: true, userAddress: "0xadmin" });
|
|
233
|
+
const query = resolversWithoutService.Query?.vetraPackages;
|
|
234
|
+
const result = await query(null, {}, ctx);
|
|
235
|
+
expect(result).toHaveLength(3);
|
|
236
|
+
});
|
|
237
|
+
});
|
|
238
|
+
describe("Unauthenticated User", () => {
|
|
239
|
+
it("should filter based on permissions for unauthenticated user", async () => {
|
|
240
|
+
setupMockQuery(mockPackages);
|
|
241
|
+
const ctx = createContext({});
|
|
242
|
+
await callVetraPackages(ctx);
|
|
243
|
+
expect(mockDocumentPermissionService.canRead).toHaveBeenCalledWith("pkg-1", undefined, expect.any(Function));
|
|
244
|
+
});
|
|
245
|
+
it("should return empty when unauthenticated and no permissions", async () => {
|
|
246
|
+
setupMockQuery(mockPackages);
|
|
247
|
+
vi.mocked(mockDocumentPermissionService.canRead).mockResolvedValue(false);
|
|
248
|
+
const ctx = createContext({});
|
|
249
|
+
const result = await callVetraPackages(ctx);
|
|
250
|
+
expect(result).toHaveLength(0);
|
|
251
|
+
});
|
|
252
|
+
});
|
|
253
|
+
});
|
|
254
|
+
describe("Permission Inheritance for Read Model", () => {
|
|
255
|
+
it("should use getParentIdsFn for hierarchy checks", async () => {
|
|
256
|
+
setupMockQuery([mockPackages[0]]);
|
|
257
|
+
const mockParents = [{ header: { id: "parent-pkg" } }];
|
|
258
|
+
vi.mocked(mockSubgraph.reactorClient.getParents).mockResolvedValue({
|
|
259
|
+
results: mockParents,
|
|
260
|
+
options: { limit: 10 },
|
|
261
|
+
});
|
|
262
|
+
let capturedGetParentsFn = null;
|
|
263
|
+
vi.mocked(mockDocumentPermissionService.canRead).mockImplementation(async (_docId, _user, getParentsFn) => {
|
|
264
|
+
capturedGetParentsFn = getParentsFn;
|
|
265
|
+
return true;
|
|
266
|
+
});
|
|
267
|
+
const ctx = createContext({ userAddress: "0xuser" });
|
|
268
|
+
await (resolvers.Query?.vetraPackages)(null, {}, ctx);
|
|
269
|
+
expect(capturedGetParentsFn).not.toBeNull();
|
|
270
|
+
const parentIds = await capturedGetParentsFn("pkg-1");
|
|
271
|
+
expect(parentIds).toEqual(["parent-pkg"]);
|
|
272
|
+
});
|
|
273
|
+
});
|
|
274
|
+
describe("AUTH_ENABLED=false behavior", () => {
|
|
275
|
+
it("should return all packages when all global roles return true", async () => {
|
|
276
|
+
setupMockQuery(mockPackages);
|
|
277
|
+
const ctx = createContext({
|
|
278
|
+
isAdmin: true,
|
|
279
|
+
isUser: true,
|
|
280
|
+
isGuest: true,
|
|
281
|
+
userAddress: "0xanyone",
|
|
282
|
+
});
|
|
283
|
+
const result = await (resolvers.Query?.vetraPackages)(null, {}, ctx);
|
|
284
|
+
expect(result).toHaveLength(3);
|
|
285
|
+
expect(mockDocumentPermissionService.canRead).not.toHaveBeenCalled();
|
|
286
|
+
});
|
|
287
|
+
});
|
|
288
|
+
describe("Edge Cases", () => {
|
|
289
|
+
it("should handle empty result set", async () => {
|
|
290
|
+
setupMockQuery([]);
|
|
291
|
+
const ctx = createContext({ isAdmin: true, userAddress: "0xadmin" });
|
|
292
|
+
const result = await (resolvers.Query?.vetraPackages)(null, {}, ctx);
|
|
293
|
+
expect(result).toHaveLength(0);
|
|
294
|
+
});
|
|
295
|
+
it("should handle packages with null optional fields", async () => {
|
|
296
|
+
setupMockQuery([mockPackages[2]]); // Package 3 has null fields
|
|
297
|
+
const ctx = createContext({ isAdmin: true, userAddress: "0xadmin" });
|
|
298
|
+
const result = await (resolvers.Query?.vetraPackages)(null, {}, ctx);
|
|
299
|
+
expect(result[0]).toMatchObject({
|
|
300
|
+
documentId: "pkg-3",
|
|
301
|
+
name: "Package 3",
|
|
302
|
+
authorWebsite: null,
|
|
303
|
+
githubUrl: null,
|
|
304
|
+
npmUrl: null,
|
|
305
|
+
});
|
|
306
|
+
});
|
|
307
|
+
it("should check permissions sequentially for each package", async () => {
|
|
308
|
+
setupMockQuery(mockPackages);
|
|
309
|
+
const callOrder = [];
|
|
310
|
+
vi.mocked(mockDocumentPermissionService.canRead).mockImplementation(async (docId) => {
|
|
311
|
+
callOrder.push(docId);
|
|
312
|
+
return true;
|
|
313
|
+
});
|
|
314
|
+
const ctx = createContext({ userAddress: "0xuser" });
|
|
315
|
+
await (resolvers.Query?.vetraPackages)(null, {}, ctx);
|
|
316
|
+
expect(callOrder).toEqual(["pkg-1", "pkg-2", "pkg-3"]);
|
|
317
|
+
});
|
|
318
|
+
});
|
|
319
|
+
});
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"resolvers.d.ts","sourceRoot":"","sources":["../../../subgraphs/app-module/resolvers.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,YAAY,
|
|
1
|
+
{"version":3,"file":"resolvers.d.ts","sourceRoot":"","sources":["../../../subgraphs/app-module/resolvers.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,YAAY,EAAW,MAAM,4BAA4B,CAAC;AA4BxE,eAAO,MAAM,YAAY,GACvB,UAAU,YAAY,KACrB,MAAM,CAAC,MAAM,EAAE,OAAO,CA6TxB,CAAC"}
|
|
@@ -1,17 +1,21 @@
|
|
|
1
1
|
import { addFile } from "document-drive";
|
|
2
2
|
import { setName } from "document-model";
|
|
3
|
+
import { GraphQLError } from "graphql";
|
|
3
4
|
import { actions, appModuleDocumentType, } from "@powerhousedao/vetra/document-models/app-module";
|
|
5
|
+
import { assertCanRead, assertCanWrite, assertCanExecuteOperation, canReadDocument, hasGlobalReadAccess, hasGlobalWriteAccess, } from "../permission-utils.js";
|
|
4
6
|
export const getResolvers = (subgraph) => {
|
|
5
7
|
const reactor = subgraph.reactor;
|
|
6
8
|
return {
|
|
7
9
|
Query: {
|
|
8
|
-
AppModule:
|
|
10
|
+
AppModule: (_, __, ctx) => {
|
|
9
11
|
return {
|
|
10
12
|
getDocument: async (args) => {
|
|
11
13
|
const { docId, driveId } = args;
|
|
12
14
|
if (!docId) {
|
|
13
15
|
throw new Error("Document id is required");
|
|
14
16
|
}
|
|
17
|
+
// Check read permission before accessing document
|
|
18
|
+
await assertCanRead(subgraph, docId, ctx);
|
|
15
19
|
if (driveId) {
|
|
16
20
|
const docIds = await reactor.getDocuments(driveId);
|
|
17
21
|
if (!docIds.includes(docId)) {
|
|
@@ -32,6 +36,8 @@ export const getResolvers = (subgraph) => {
|
|
|
32
36
|
},
|
|
33
37
|
getDocuments: async (args) => {
|
|
34
38
|
const { driveId } = args;
|
|
39
|
+
// Check read permission on drive before listing documents
|
|
40
|
+
await assertCanRead(subgraph, driveId, ctx);
|
|
35
41
|
const docsIds = await reactor.getDocuments(driveId);
|
|
36
42
|
const docs = await Promise.all(docsIds.map(async (docId) => {
|
|
37
43
|
const doc = await reactor.getDocument(docId);
|
|
@@ -46,14 +52,34 @@ export const getResolvers = (subgraph) => {
|
|
|
46
52
|
revision: doc.header?.revision?.global ?? 0,
|
|
47
53
|
};
|
|
48
54
|
}));
|
|
49
|
-
|
|
55
|
+
const filteredByType = docs.filter((doc) => doc.header.documentType === appModuleDocumentType);
|
|
56
|
+
// If user doesn't have global read access, filter by document-level permissions
|
|
57
|
+
if (!hasGlobalReadAccess(ctx) &&
|
|
58
|
+
subgraph.documentPermissionService) {
|
|
59
|
+
const filteredDocs = [];
|
|
60
|
+
for (const doc of filteredByType) {
|
|
61
|
+
const canRead = await canReadDocument(subgraph, doc.id, ctx);
|
|
62
|
+
if (canRead) {
|
|
63
|
+
filteredDocs.push(doc);
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
return filteredDocs;
|
|
67
|
+
}
|
|
68
|
+
return filteredByType;
|
|
50
69
|
},
|
|
51
70
|
};
|
|
52
71
|
},
|
|
53
72
|
},
|
|
54
73
|
Mutation: {
|
|
55
|
-
AppModule_createDocument: async (_, args) => {
|
|
74
|
+
AppModule_createDocument: async (_, args, ctx) => {
|
|
56
75
|
const { driveId, name } = args;
|
|
76
|
+
// If creating under a drive, check write permission on drive
|
|
77
|
+
if (driveId) {
|
|
78
|
+
await assertCanWrite(subgraph, driveId, ctx);
|
|
79
|
+
}
|
|
80
|
+
else if (!hasGlobalWriteAccess(ctx)) {
|
|
81
|
+
throw new GraphQLError("Forbidden: insufficient permissions to create documents");
|
|
82
|
+
}
|
|
57
83
|
const document = await reactor.addDocument(appModuleDocumentType);
|
|
58
84
|
if (driveId) {
|
|
59
85
|
await reactor.addAction(driveId, addFile({
|
|
@@ -67,8 +93,11 @@ export const getResolvers = (subgraph) => {
|
|
|
67
93
|
}
|
|
68
94
|
return document.header.id;
|
|
69
95
|
},
|
|
70
|
-
AppModule_setAppName: async (_, args) => {
|
|
96
|
+
AppModule_setAppName: async (_, args, ctx) => {
|
|
71
97
|
const { docId, input } = args;
|
|
98
|
+
// Check write permission before mutating document
|
|
99
|
+
await assertCanWrite(subgraph, docId, ctx);
|
|
100
|
+
await assertCanExecuteOperation(subgraph, docId, "SET_APP_NAME", ctx);
|
|
72
101
|
const doc = await reactor.getDocument(docId);
|
|
73
102
|
if (!doc) {
|
|
74
103
|
throw new Error("Document not found");
|
|
@@ -79,8 +108,11 @@ export const getResolvers = (subgraph) => {
|
|
|
79
108
|
}
|
|
80
109
|
return true;
|
|
81
110
|
},
|
|
82
|
-
AppModule_setAppStatus: async (_, args) => {
|
|
111
|
+
AppModule_setAppStatus: async (_, args, ctx) => {
|
|
83
112
|
const { docId, input } = args;
|
|
113
|
+
// Check write permission before mutating document
|
|
114
|
+
await assertCanWrite(subgraph, docId, ctx);
|
|
115
|
+
await assertCanExecuteOperation(subgraph, docId, "SET_APP_STATUS", ctx);
|
|
84
116
|
const doc = await reactor.getDocument(docId);
|
|
85
117
|
if (!doc) {
|
|
86
118
|
throw new Error("Document not found");
|
|
@@ -91,8 +123,11 @@ export const getResolvers = (subgraph) => {
|
|
|
91
123
|
}
|
|
92
124
|
return true;
|
|
93
125
|
},
|
|
94
|
-
AppModule_addDocumentType: async (_, args) => {
|
|
126
|
+
AppModule_addDocumentType: async (_, args, ctx) => {
|
|
95
127
|
const { docId, input } = args;
|
|
128
|
+
// Check write permission before mutating document
|
|
129
|
+
await assertCanWrite(subgraph, docId, ctx);
|
|
130
|
+
await assertCanExecuteOperation(subgraph, docId, "ADD_DOCUMENT_TYPE", ctx);
|
|
96
131
|
const doc = await reactor.getDocument(docId);
|
|
97
132
|
if (!doc) {
|
|
98
133
|
throw new Error("Document not found");
|
|
@@ -103,8 +138,11 @@ export const getResolvers = (subgraph) => {
|
|
|
103
138
|
}
|
|
104
139
|
return true;
|
|
105
140
|
},
|
|
106
|
-
AppModule_removeDocumentType: async (_, args) => {
|
|
141
|
+
AppModule_removeDocumentType: async (_, args, ctx) => {
|
|
107
142
|
const { docId, input } = args;
|
|
143
|
+
// Check write permission before mutating document
|
|
144
|
+
await assertCanWrite(subgraph, docId, ctx);
|
|
145
|
+
await assertCanExecuteOperation(subgraph, docId, "REMOVE_DOCUMENT_TYPE", ctx);
|
|
108
146
|
const doc = await reactor.getDocument(docId);
|
|
109
147
|
if (!doc) {
|
|
110
148
|
throw new Error("Document not found");
|
|
@@ -115,8 +153,11 @@ export const getResolvers = (subgraph) => {
|
|
|
115
153
|
}
|
|
116
154
|
return true;
|
|
117
155
|
},
|
|
118
|
-
AppModule_setDocumentTypes: async (_, args) => {
|
|
156
|
+
AppModule_setDocumentTypes: async (_, args, ctx) => {
|
|
119
157
|
const { docId, input } = args;
|
|
158
|
+
// Check write permission before mutating document
|
|
159
|
+
await assertCanWrite(subgraph, docId, ctx);
|
|
160
|
+
await assertCanExecuteOperation(subgraph, docId, "SET_DOCUMENT_TYPES", ctx);
|
|
120
161
|
const doc = await reactor.getDocument(docId);
|
|
121
162
|
if (!doc) {
|
|
122
163
|
throw new Error("Document not found");
|
|
@@ -127,8 +168,11 @@ export const getResolvers = (subgraph) => {
|
|
|
127
168
|
}
|
|
128
169
|
return true;
|
|
129
170
|
},
|
|
130
|
-
AppModule_setDragAndDropEnabled: async (_, args) => {
|
|
171
|
+
AppModule_setDragAndDropEnabled: async (_, args, ctx) => {
|
|
131
172
|
const { docId, input } = args;
|
|
173
|
+
// Check write permission before mutating document
|
|
174
|
+
await assertCanWrite(subgraph, docId, ctx);
|
|
175
|
+
await assertCanExecuteOperation(subgraph, docId, "SET_DRAG_AND_DROP_ENABLED", ctx);
|
|
132
176
|
const doc = await reactor.getDocument(docId);
|
|
133
177
|
if (!doc) {
|
|
134
178
|
throw new Error("Document not found");
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"resolvers.d.ts","sourceRoot":"","sources":["../../../subgraphs/document-editor/resolvers.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,YAAY,
|
|
1
|
+
{"version":3,"file":"resolvers.d.ts","sourceRoot":"","sources":["../../../subgraphs/document-editor/resolvers.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,YAAY,EAAW,MAAM,4BAA4B,CAAC;AA0BxE,eAAO,MAAM,YAAY,GACvB,UAAU,YAAY,KACrB,MAAM,CAAC,MAAM,EAAE,OAAO,CAmQxB,CAAC"}
|
|
@@ -1,17 +1,21 @@
|
|
|
1
1
|
import { addFile } from "document-drive";
|
|
2
2
|
import { setName } from "document-model";
|
|
3
|
+
import { GraphQLError } from "graphql";
|
|
3
4
|
import { actions, documentEditorDocumentType, } from "@powerhousedao/vetra/document-models/document-editor";
|
|
5
|
+
import { assertCanRead, assertCanWrite, assertCanExecuteOperation, canReadDocument, hasGlobalReadAccess, hasGlobalWriteAccess, } from "../permission-utils.js";
|
|
4
6
|
export const getResolvers = (subgraph) => {
|
|
5
7
|
const reactor = subgraph.reactor;
|
|
6
8
|
return {
|
|
7
9
|
Query: {
|
|
8
|
-
DocumentEditor:
|
|
10
|
+
DocumentEditor: (_, __, ctx) => {
|
|
9
11
|
return {
|
|
10
12
|
getDocument: async (args) => {
|
|
11
13
|
const { docId, driveId } = args;
|
|
12
14
|
if (!docId) {
|
|
13
15
|
throw new Error("Document id is required");
|
|
14
16
|
}
|
|
17
|
+
// Check read permission before accessing document
|
|
18
|
+
await assertCanRead(subgraph, docId, ctx);
|
|
15
19
|
if (driveId) {
|
|
16
20
|
const docIds = await reactor.getDocuments(driveId);
|
|
17
21
|
if (!docIds.includes(docId)) {
|
|
@@ -32,6 +36,8 @@ export const getResolvers = (subgraph) => {
|
|
|
32
36
|
},
|
|
33
37
|
getDocuments: async (args) => {
|
|
34
38
|
const { driveId } = args;
|
|
39
|
+
// Check read permission on drive before listing documents
|
|
40
|
+
await assertCanRead(subgraph, driveId, ctx);
|
|
35
41
|
const docsIds = await reactor.getDocuments(driveId);
|
|
36
42
|
const docs = await Promise.all(docsIds.map(async (docId) => {
|
|
37
43
|
const doc = await reactor.getDocument(docId);
|
|
@@ -46,14 +52,34 @@ export const getResolvers = (subgraph) => {
|
|
|
46
52
|
revision: doc.header?.revision?.global ?? 0,
|
|
47
53
|
};
|
|
48
54
|
}));
|
|
49
|
-
|
|
55
|
+
const filteredByType = docs.filter((doc) => doc.header.documentType === documentEditorDocumentType);
|
|
56
|
+
// If user doesn't have global read access, filter by document-level permissions
|
|
57
|
+
if (!hasGlobalReadAccess(ctx) &&
|
|
58
|
+
subgraph.documentPermissionService) {
|
|
59
|
+
const filteredDocs = [];
|
|
60
|
+
for (const doc of filteredByType) {
|
|
61
|
+
const canRead = await canReadDocument(subgraph, doc.id, ctx);
|
|
62
|
+
if (canRead) {
|
|
63
|
+
filteredDocs.push(doc);
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
return filteredDocs;
|
|
67
|
+
}
|
|
68
|
+
return filteredByType;
|
|
50
69
|
},
|
|
51
70
|
};
|
|
52
71
|
},
|
|
53
72
|
},
|
|
54
73
|
Mutation: {
|
|
55
|
-
DocumentEditor_createDocument: async (_, args) => {
|
|
74
|
+
DocumentEditor_createDocument: async (_, args, ctx) => {
|
|
56
75
|
const { driveId, name } = args;
|
|
76
|
+
// If creating under a drive, check write permission on drive
|
|
77
|
+
if (driveId) {
|
|
78
|
+
await assertCanWrite(subgraph, driveId, ctx);
|
|
79
|
+
}
|
|
80
|
+
else if (!hasGlobalWriteAccess(ctx)) {
|
|
81
|
+
throw new GraphQLError("Forbidden: insufficient permissions to create documents");
|
|
82
|
+
}
|
|
57
83
|
const document = await reactor.addDocument(documentEditorDocumentType);
|
|
58
84
|
if (driveId) {
|
|
59
85
|
await reactor.addAction(driveId, addFile({
|
|
@@ -67,8 +93,11 @@ export const getResolvers = (subgraph) => {
|
|
|
67
93
|
}
|
|
68
94
|
return document.header.id;
|
|
69
95
|
},
|
|
70
|
-
DocumentEditor_setEditorName: async (_, args) => {
|
|
96
|
+
DocumentEditor_setEditorName: async (_, args, ctx) => {
|
|
71
97
|
const { docId, input } = args;
|
|
98
|
+
// Check write permission before mutating document
|
|
99
|
+
await assertCanWrite(subgraph, docId, ctx);
|
|
100
|
+
await assertCanExecuteOperation(subgraph, docId, "SET_EDITOR_NAME", ctx);
|
|
72
101
|
const doc = await reactor.getDocument(docId);
|
|
73
102
|
if (!doc) {
|
|
74
103
|
throw new Error("Document not found");
|
|
@@ -79,8 +108,11 @@ export const getResolvers = (subgraph) => {
|
|
|
79
108
|
}
|
|
80
109
|
return true;
|
|
81
110
|
},
|
|
82
|
-
DocumentEditor_addDocumentType: async (_, args) => {
|
|
111
|
+
DocumentEditor_addDocumentType: async (_, args, ctx) => {
|
|
83
112
|
const { docId, input } = args;
|
|
113
|
+
// Check write permission before mutating document
|
|
114
|
+
await assertCanWrite(subgraph, docId, ctx);
|
|
115
|
+
await assertCanExecuteOperation(subgraph, docId, "ADD_DOCUMENT_TYPE", ctx);
|
|
84
116
|
const doc = await reactor.getDocument(docId);
|
|
85
117
|
if (!doc) {
|
|
86
118
|
throw new Error("Document not found");
|
|
@@ -91,8 +123,11 @@ export const getResolvers = (subgraph) => {
|
|
|
91
123
|
}
|
|
92
124
|
return true;
|
|
93
125
|
},
|
|
94
|
-
DocumentEditor_removeDocumentType: async (_, args) => {
|
|
126
|
+
DocumentEditor_removeDocumentType: async (_, args, ctx) => {
|
|
95
127
|
const { docId, input } = args;
|
|
128
|
+
// Check write permission before mutating document
|
|
129
|
+
await assertCanWrite(subgraph, docId, ctx);
|
|
130
|
+
await assertCanExecuteOperation(subgraph, docId, "REMOVE_DOCUMENT_TYPE", ctx);
|
|
96
131
|
const doc = await reactor.getDocument(docId);
|
|
97
132
|
if (!doc) {
|
|
98
133
|
throw new Error("Document not found");
|
|
@@ -103,8 +138,11 @@ export const getResolvers = (subgraph) => {
|
|
|
103
138
|
}
|
|
104
139
|
return true;
|
|
105
140
|
},
|
|
106
|
-
DocumentEditor_setEditorStatus: async (_, args) => {
|
|
141
|
+
DocumentEditor_setEditorStatus: async (_, args, ctx) => {
|
|
107
142
|
const { docId, input } = args;
|
|
143
|
+
// Check write permission before mutating document
|
|
144
|
+
await assertCanWrite(subgraph, docId, ctx);
|
|
145
|
+
await assertCanExecuteOperation(subgraph, docId, "SET_EDITOR_STATUS", ctx);
|
|
108
146
|
const doc = await reactor.getDocument(docId);
|
|
109
147
|
if (!doc) {
|
|
110
148
|
throw new Error("Document not found");
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import type { BaseSubgraph, Context } from "@powerhousedao/reactor-api";
|
|
2
|
+
/**
|
|
3
|
+
* Check if user has global read access (admin, user, or guest)
|
|
4
|
+
*/
|
|
5
|
+
export declare function hasGlobalReadAccess(ctx: Context): boolean;
|
|
6
|
+
/**
|
|
7
|
+
* Check if user has global write access (admin or user, not guest)
|
|
8
|
+
*/
|
|
9
|
+
export declare function hasGlobalWriteAccess(ctx: Context): boolean;
|
|
10
|
+
/**
|
|
11
|
+
* Check if user can read a document (with hierarchy)
|
|
12
|
+
*/
|
|
13
|
+
export declare function canReadDocument(subgraph: BaseSubgraph, documentId: string, ctx: Context): Promise<boolean>;
|
|
14
|
+
/**
|
|
15
|
+
* Check if user can write to a document (with hierarchy)
|
|
16
|
+
*/
|
|
17
|
+
export declare function canWriteDocument(subgraph: BaseSubgraph, documentId: string, ctx: Context): Promise<boolean>;
|
|
18
|
+
/**
|
|
19
|
+
* Throw an error if user cannot read the document
|
|
20
|
+
*/
|
|
21
|
+
export declare function assertCanRead(subgraph: BaseSubgraph, documentId: string, ctx: Context): Promise<void>;
|
|
22
|
+
/**
|
|
23
|
+
* Throw an error if user cannot write to the document
|
|
24
|
+
*/
|
|
25
|
+
export declare function assertCanWrite(subgraph: BaseSubgraph, documentId: string, ctx: Context): Promise<void>;
|
|
26
|
+
/**
|
|
27
|
+
* Check if user can execute a specific operation on a document.
|
|
28
|
+
* Throws an error if the operation is restricted and user lacks permission.
|
|
29
|
+
*/
|
|
30
|
+
export declare function assertCanExecuteOperation(subgraph: BaseSubgraph, documentId: string, operationType: string, ctx: Context): Promise<void>;
|
|
31
|
+
//# sourceMappingURL=permission-utils.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"permission-utils.d.ts","sourceRoot":"","sources":["../../subgraphs/permission-utils.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,YAAY,EAAE,OAAO,EAAE,MAAM,4BAA4B,CAAC;AAGxE;;GAEG;AACH,wBAAgB,mBAAmB,CAAC,GAAG,EAAE,OAAO,GAAG,OAAO,CAMzD;AAED;;GAEG;AACH,wBAAgB,oBAAoB,CAAC,GAAG,EAAE,OAAO,GAAG,OAAO,CAI1D;AAgBD;;GAEG;AACH,wBAAsB,eAAe,CACnC,QAAQ,EAAE,YAAY,EACtB,UAAU,EAAE,MAAM,EAClB,GAAG,EAAE,OAAO,GACX,OAAO,CAAC,OAAO,CAAC,CAgBlB;AAED;;GAEG;AACH,wBAAsB,gBAAgB,CACpC,QAAQ,EAAE,YAAY,EACtB,UAAU,EAAE,MAAM,EAClB,GAAG,EAAE,OAAO,GACX,OAAO,CAAC,OAAO,CAAC,CAgBlB;AAED;;GAEG;AACH,wBAAsB,aAAa,CACjC,QAAQ,EAAE,YAAY,EACtB,UAAU,EAAE,MAAM,EAClB,GAAG,EAAE,OAAO,GACX,OAAO,CAAC,IAAI,CAAC,CAOf;AAED;;GAEG;AACH,wBAAsB,cAAc,CAClC,QAAQ,EAAE,YAAY,EACtB,UAAU,EAAE,MAAM,EAClB,GAAG,EAAE,OAAO,GACX,OAAO,CAAC,IAAI,CAAC,CAOf;AAED;;;GAGG;AACH,wBAAsB,yBAAyB,CAC7C,QAAQ,EAAE,YAAY,EACtB,UAAU,EAAE,MAAM,EAClB,aAAa,EAAE,MAAM,EACrB,GAAG,EAAE,OAAO,GACX,OAAO,CAAC,IAAI,CAAC,CAiCf"}
|