Skip to content

Commit d8e52d4

Browse files
authored
Feat/add x ename header support (#405)
* feat: move to x-ename header and add tests * chore: add web3-adapter e2e tests * chore: ran formatter * chore: fix tests e2e tests * chore: prevent overlap on e2e and unit tests * fix: unit tests failure * chore: fix tests e2e tests
1 parent 0d77158 commit d8e52d4

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

54 files changed

+1330
-1900
lines changed
Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,58 @@
1+
name: Tests [evault-core + web3-adapter Integration]
2+
3+
on:
4+
push:
5+
branches: [main]
6+
pull_request:
7+
branches: [main]
8+
paths:
9+
- 'infrastructure/evault-core/**'
10+
- 'infrastructure/web3-adapter/**'
11+
12+
jobs:
13+
test-web3-adapter-integration:
14+
runs-on: ubuntu-latest
15+
16+
steps:
17+
- name: Checkout code
18+
uses: actions/checkout@v4
19+
20+
- name: Set up Node.js 22
21+
uses: actions/setup-node@v4
22+
with:
23+
node-version: 22
24+
25+
- name: Install build dependencies
26+
run: |
27+
sudo apt-get update
28+
sudo apt-get install -y build-essential python3
29+
30+
- name: Install pnpm
31+
run: npm install -g pnpm
32+
33+
- name: Install dependencies
34+
run: pnpm install
35+
36+
- name: Clean and rebuild native modules
37+
run: |
38+
# Remove any pre-built binaries that might be incompatible
39+
find node_modules -name "sshcrypto.node" -delete 2>/dev/null || true
40+
find node_modules -path "*/ssh2/lib/protocol/crypto/build/Release/sshcrypto.node" -delete 2>/dev/null || true
41+
# Rebuild ssh2 specifically for this platform
42+
pnpm rebuild ssh2
43+
# Rebuild all other native modules
44+
pnpm rebuild
45+
46+
- name: Build web3-adapter
47+
run: pnpm -F=web3-adapter build
48+
49+
- name: Run evault-core + web3-adapter integration tests
50+
env:
51+
CI: true
52+
GITHUB_ACTIONS: true
53+
DOCKER_HOST: unix:///var/run/docker.sock
54+
TESTCONTAINERS_DOCKER_SOCKET_OVERRIDE: /var/run/docker.sock
55+
TESTCONTAINERS_RYUK_DISABLED: false
56+
TESTCONTAINERS_HOST_OVERRIDE: localhost
57+
run: pnpm -F=evault-core test:e2e:web3-adapter
58+

.github/workflows/tests-evault-core.yml

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,8 +3,6 @@ name: Tests [evault-core]
33
on:
44
push:
55
branches: [main]
6-
paths:
7-
- 'infrastructure/evault-core/**'
86
pull_request:
97
branches: [main]
108
paths:

.github/workflows/tests-registry.yml

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,8 +3,6 @@ name: Tests [registry]
33
on:
44
push:
55
branches: [main]
6-
paths:
7-
- 'platforms/registry/**'
86
pull_request:
97
branches: [main]
108
paths:
Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
name: Tests [web3-adapter]
2+
3+
on:
4+
push:
5+
branches: [main]
6+
pull_request:
7+
branches: [main]
8+
paths:
9+
- 'infrastructure/web3-adapter/**'
10+
11+
jobs:
12+
test:
13+
runs-on: ubuntu-latest
14+
15+
steps:
16+
- name: Checkout code
17+
uses: actions/checkout@v3
18+
19+
- name: Set up Node.js 22
20+
uses: actions/setup-node@v4
21+
with:
22+
node-version: 22
23+
24+
- name: Install build dependencies
25+
run: |
26+
sudo apt-get update
27+
sudo apt-get install -y build-essential python3
28+
29+
- name: Install pnpm
30+
run: npm install -g pnpm
31+
32+
- name: Install dependencies
33+
run: pnpm install
34+
35+
- name: Clean and rebuild ssh2 native module
36+
run: |
37+
# Remove any pre-built binaries that might be incompatible
38+
find node_modules -name "sshcrypto.node" -delete 2>/dev/null || true
39+
find node_modules -path "*/ssh2/lib/protocol/crypto/build/Release/sshcrypto.node" -delete 2>/dev/null || true
40+
# Rebuild ssh2 specifically for this platform
41+
pnpm rebuild ssh2
42+
# Rebuild all other native modules
43+
pnpm rebuild
44+
45+
- name: Run tests
46+
run: pnpm -F=web3-adapter test --run
47+

infrastructure/evault-core/package.json

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,9 @@
77
"start": "node dist/index.js",
88
"dev": "ts-node-dev --respawn --transpile-only src/index.ts",
99
"build": "tsc",
10-
"test": "vitest",
10+
"test": "vitest --exclude '**/e2e/**'",
11+
"test:e2e": "vitest src/e2e/evault-core.e2e.spec.ts --run --config vitest.config.e2e.ts",
12+
"test:e2e:web3-adapter": "vitest src/e2e/evault-core.e2e.spec.ts --run --config vitest.config.e2e.ts",
1113
"typeorm": "typeorm-ts-node-commonjs",
1214
"migration:generate": "npm run typeorm migration:generate -- -d src/config/database.ts",
1315
"migration:run": "npm run typeorm migration:run -- -d src/config/database.ts",

infrastructure/evault-core/src/core/protocol/graphql-server.ts

Lines changed: 23 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -196,6 +196,12 @@ export class GraphQLServer {
196196
context.eName
197197
);
198198

199+
// Add parsed field to metaEnvelope for GraphQL response
200+
const metaEnvelopeWithParsed = {
201+
...result.metaEnvelope,
202+
parsed: input.payload,
203+
};
204+
199205
// Deliver webhooks for create operation
200206
const requestingPlatform =
201207
context.tokenPayload?.platform || null;
@@ -224,7 +230,10 @@ export class GraphQLServer {
224230
);
225231
}, 3_000);
226232

227-
return result;
233+
return {
234+
...result,
235+
metaEnvelope: metaEnvelopeWithParsed,
236+
};
228237
}
229238
),
230239
updateMetaEnvelopeById: this.accessGuard.middleware(
@@ -330,11 +339,19 @@ export class GraphQLServer {
330339
const eName = request.headers.get("x-ename") ?? request.headers.get("X-ENAME") ?? null;
331340

332341
if (token) {
333-
const id = getJWTHeader(token).kid?.split("#")[0];
334-
return {
335-
currentUser: id ?? null,
336-
eName: eName,
337-
};
342+
try {
343+
const id = getJWTHeader(token).kid?.split("#")[0];
344+
return {
345+
currentUser: id ?? null,
346+
eName: eName,
347+
};
348+
} catch (error) {
349+
// Invalid JWT token - ignore and continue without currentUser
350+
return {
351+
currentUser: null,
352+
eName: eName,
353+
};
354+
}
338355
}
339356

340357
return {

infrastructure/evault-core/src/core/protocol/vault-access-guard.spec.ts

Lines changed: 15 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -136,7 +136,7 @@ describe("VaultAccessGuard", () => {
136136
const checkAccess = (guard as any).checkAccess.bind(guard);
137137
const result = await checkAccess("meta-envelope-id", context);
138138

139-
expect(result).toBe(true);
139+
expect(result.hasAccess).toBe(true);
140140
expect(context.tokenPayload).toBeDefined();
141141
});
142142

@@ -160,7 +160,7 @@ describe("VaultAccessGuard", () => {
160160
const checkAccess = (guard as any).checkAccess.bind(guard);
161161
const result = await checkAccess(metaEnvelope.metaEnvelope.id, context);
162162

163-
expect(result).toBe(true);
163+
expect(result.hasAccess).toBe(true);
164164
});
165165

166166
it("should allow access when user is in ACL", async () => {
@@ -183,7 +183,7 @@ describe("VaultAccessGuard", () => {
183183
const checkAccess = (guard as any).checkAccess.bind(guard);
184184
const result = await checkAccess(metaEnvelope.metaEnvelope.id, context);
185185

186-
expect(result).toBe(true);
186+
expect(result.hasAccess).toBe(true);
187187
});
188188

189189
it("should deny access when user is not in ACL", async () => {
@@ -206,7 +206,7 @@ describe("VaultAccessGuard", () => {
206206
const checkAccess = (guard as any).checkAccess.bind(guard);
207207
const result = await checkAccess(metaEnvelope.metaEnvelope.id, context);
208208

209-
expect(result).toBe(false);
209+
expect(result.hasAccess).toBe(false);
210210
});
211211

212212
it("should throw error when eName header is missing", async () => {
@@ -247,7 +247,8 @@ describe("VaultAccessGuard", () => {
247247

248248
// Should return false because the meta-envelope won't be found with eName2
249249
const result = await checkAccess(metaEnvelope.metaEnvelope.id, context);
250-
expect(result).toBe(false);
250+
expect(result.hasAccess).toBe(false);
251+
expect(result.exists).toBe(false);
251252
});
252253

253254
it("should allow access only to meta-envelopes matching the provided eName", async () => {
@@ -284,11 +285,11 @@ describe("VaultAccessGuard", () => {
284285
const checkAccess = (guard as any).checkAccess.bind(guard);
285286

286287
const result1 = await checkAccess(metaEnvelope1.metaEnvelope.id, context1);
287-
expect(result1).toBe(true);
288+
expect(result1.hasAccess).toBe(true);
288289

289290
// Tenant1 should NOT access tenant2's data
290291
const result2 = await checkAccess(metaEnvelope2.metaEnvelope.id, context1);
291-
expect(result2).toBe(false);
292+
expect(result2.hasAccess).toBe(false);
292293

293294
// Tenant2 should only access their own data
294295
const context2 = createMockContext({
@@ -297,11 +298,11 @@ describe("VaultAccessGuard", () => {
297298
});
298299

299300
const result3 = await checkAccess(metaEnvelope2.metaEnvelope.id, context2);
300-
expect(result3).toBe(true);
301+
expect(result3.hasAccess).toBe(true);
301302

302303
// Tenant2 should NOT access tenant1's data
303304
const result4 = await checkAccess(metaEnvelope1.metaEnvelope.id, context2);
304-
expect(result4).toBe(false);
305+
expect(result4.hasAccess).toBe(false);
305306
});
306307

307308
it("should allow access with ACL '*' even without currentUser", async () => {
@@ -324,7 +325,8 @@ describe("VaultAccessGuard", () => {
324325
const checkAccess = (guard as any).checkAccess.bind(guard);
325326
const result = await checkAccess(metaEnvelope.metaEnvelope.id, context);
326327

327-
expect(result).toBe(true);
328+
expect(result.hasAccess).toBe(true);
329+
expect(result.exists).toBe(true);
328330
});
329331
});
330332

@@ -420,10 +422,9 @@ describe("VaultAccessGuard", () => {
420422

421423
const wrappedResolver = guard.middleware(mockResolver);
422424

423-
// Should throw "Access denied" because the meta-envelope won't be found with eName2
424-
await expect(
425-
wrappedResolver(null, { id: metaEnvelope.metaEnvelope.id }, context)
426-
).rejects.toThrow("Access denied");
425+
// When envelope doesn't exist (wrong eName), middleware returns null (not found)
426+
const result = await wrappedResolver(null, { id: metaEnvelope.metaEnvelope.id }, context);
427+
expect(result).toBeNull();
427428
});
428429
});
429430
});

infrastructure/evault-core/src/core/protocol/vault-access-guard.ts

Lines changed: 41 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -54,12 +54,12 @@ export class VaultAccessGuard {
5454
* Checks if the current user has access to a meta envelope based on its ACL
5555
* @param metaEnvelopeId - The ID of the meta envelope to check access for
5656
* @param context - The GraphQL context containing the current user
57-
* @returns Promise<boolean> - Whether the user has access
57+
* @returns Promise<{hasAccess: boolean, exists: boolean}> - Whether the user has access and if envelope exists
5858
*/
5959
private async checkAccess(
6060
metaEnvelopeId: string,
6161
context: VaultContext
62-
): Promise<boolean> {
62+
): Promise<{ hasAccess: boolean; exists: boolean }> {
6363
// Validate token if present
6464
const authHeader =
6565
context.request?.headers?.get("authorization") ??
@@ -69,36 +69,40 @@ export class VaultAccessGuard {
6969
if (tokenPayload) {
7070
// Token is valid, set platform context and allow access
7171
context.tokenPayload = tokenPayload;
72-
return true;
72+
// Still need to check if envelope exists
73+
if (!context.eName) {
74+
return { hasAccess: true, exists: false };
75+
}
76+
const metaEnvelope = await this.db.findMetaEnvelopeById(metaEnvelopeId, context.eName);
77+
return { hasAccess: true, exists: metaEnvelope !== null };
7378
}
7479

7580
// Validate eName is present
7681
if (!context.eName) {
7782
throw new Error("X-ENAME header is required for access control");
7883
}
7984

80-
// Fallback to original ACL logic if no valid token
81-
if (!context.currentUser) {
82-
const metaEnvelope = await this.db.findMetaEnvelopeById(
83-
metaEnvelopeId,
84-
context.eName
85-
);
86-
if (metaEnvelope && metaEnvelope.acl.includes("*")) return true;
87-
return false;
88-
}
89-
9085
const metaEnvelope = await this.db.findMetaEnvelopeById(metaEnvelopeId, context.eName);
9186
if (!metaEnvelope) {
92-
return false;
87+
return { hasAccess: false, exists: false };
88+
}
89+
90+
// Fallback to original ACL logic if no valid token
91+
if (!context.currentUser) {
92+
if (metaEnvelope.acl.includes("*")) {
93+
return { hasAccess: true, exists: true };
94+
}
95+
return { hasAccess: false, exists: true };
9396
}
9497

9598
// If ACL contains "*", anyone can access
9699
if (metaEnvelope.acl.includes("*")) {
97-
return true;
100+
return { hasAccess: true, exists: true };
98101
}
99102

100103
// Check if the current user's ID is in the ACL
101-
return metaEnvelope.acl.includes(context.currentUser);
104+
const hasAccess = metaEnvelope.acl.includes(context.currentUser);
105+
return { hasAccess, exists: true };
102106
}
103107

104108
/**
@@ -147,8 +151,14 @@ export class VaultAccessGuard {
147151
if (!args.id && !args.envelopeId) {
148152
const result = await resolver(parent, args, context);
149153

150-
// If the result is an array of meta envelopes, filter based on access
154+
// If the result is an array
151155
if (Array.isArray(result)) {
156+
// Check if it's an array of Envelopes (no ACL) or MetaEnvelopes (has ACL)
157+
if (result.length > 0 && result[0] && !('acl' in result[0])) {
158+
// It's an array of Envelopes - already filtered by eName, just return as-is
159+
return result;
160+
}
161+
// It's an array of MetaEnvelopes - filter based on access
152162
return this.filterEnvelopesByAccess(result, context);
153163
}
154164

@@ -163,13 +173,25 @@ export class VaultAccessGuard {
163173
return this.filterACL(result);
164174
}
165175

166-
const hasAccess = await this.checkAccess(metaEnvelopeId, context);
176+
// Check if envelope exists and user has access
177+
const { hasAccess, exists } = await this.checkAccess(metaEnvelopeId, context);
167178
if (!hasAccess) {
179+
// If envelope doesn't exist, return null (not found)
180+
if (!exists) {
181+
return null;
182+
}
183+
// Envelope exists but access denied
168184
throw new Error("Access denied");
169185
}
170186

171-
// console.log
187+
// Execute resolver and filter ACL
172188
const result = await resolver(parent, args, context);
189+
190+
// If result is null (envelope not found), return null
191+
if (result === null) {
192+
return null;
193+
}
194+
173195
return this.filterACL(result);
174196
};
175197
}

0 commit comments

Comments
 (0)