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
58 changes: 58 additions & 0 deletions .github/workflows/tests-evault-core-e2e.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
name: Tests [evault-core + web3-adapter Integration]

on:
push:
branches: [main]
pull_request:
branches: [main]
paths:
- 'infrastructure/evault-core/**'
- 'infrastructure/web3-adapter/**'

jobs:
test-web3-adapter-integration:
runs-on: ubuntu-latest

steps:
- name: Checkout code
uses: actions/checkout@v4

- name: Set up Node.js 22
uses: actions/setup-node@v4
with:
node-version: 22

- name: Install build dependencies
run: |
sudo apt-get update
sudo apt-get install -y build-essential python3

- name: Install pnpm
run: npm install -g pnpm

- name: Install dependencies
run: pnpm install

- name: Clean and rebuild native modules
run: |
# Remove any pre-built binaries that might be incompatible
find node_modules -name "sshcrypto.node" -delete 2>/dev/null || true
find node_modules -path "*/ssh2/lib/protocol/crypto/build/Release/sshcrypto.node" -delete 2>/dev/null || true
# Rebuild ssh2 specifically for this platform
pnpm rebuild ssh2
# Rebuild all other native modules
pnpm rebuild

- name: Build web3-adapter
run: pnpm -F=web3-adapter build

- name: Run evault-core + web3-adapter integration tests
env:
CI: true
GITHUB_ACTIONS: true
DOCKER_HOST: unix:///var/run/docker.sock
TESTCONTAINERS_DOCKER_SOCKET_OVERRIDE: /var/run/docker.sock
TESTCONTAINERS_RYUK_DISABLED: false
TESTCONTAINERS_HOST_OVERRIDE: localhost
run: pnpm -F=evault-core test:e2e:web3-adapter

2 changes: 0 additions & 2 deletions .github/workflows/tests-evault-core.yml
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,6 @@ name: Tests [evault-core]
on:
push:
branches: [main]
paths:
- 'infrastructure/evault-core/**'
pull_request:
branches: [main]
paths:
Expand Down
2 changes: 0 additions & 2 deletions .github/workflows/tests-registry.yml
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,6 @@ name: Tests [registry]
on:
push:
branches: [main]
paths:
- 'platforms/registry/**'
pull_request:
branches: [main]
paths:
Expand Down
47 changes: 47 additions & 0 deletions .github/workflows/tests-web3-adapter.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
name: Tests [web3-adapter]

on:
push:
branches: [main]
pull_request:
branches: [main]
paths:
- 'infrastructure/web3-adapter/**'

jobs:
test:
runs-on: ubuntu-latest

steps:
- name: Checkout code
uses: actions/checkout@v3

- name: Set up Node.js 22
uses: actions/setup-node@v4
with:
node-version: 22

- name: Install build dependencies
run: |
sudo apt-get update
sudo apt-get install -y build-essential python3

- name: Install pnpm
run: npm install -g pnpm

- name: Install dependencies
run: pnpm install

- name: Clean and rebuild ssh2 native module
run: |
# Remove any pre-built binaries that might be incompatible
find node_modules -name "sshcrypto.node" -delete 2>/dev/null || true
find node_modules -path "*/ssh2/lib/protocol/crypto/build/Release/sshcrypto.node" -delete 2>/dev/null || true
# Rebuild ssh2 specifically for this platform
pnpm rebuild ssh2
# Rebuild all other native modules
pnpm rebuild

- name: Run tests
run: pnpm -F=web3-adapter test --run

4 changes: 3 additions & 1 deletion infrastructure/evault-core/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,9 @@
"start": "node dist/index.js",
"dev": "ts-node-dev --respawn --transpile-only src/index.ts",
"build": "tsc",
"test": "vitest",
"test": "vitest --exclude '**/e2e/**'",
"test:e2e": "vitest src/e2e/evault-core.e2e.spec.ts --run --config vitest.config.e2e.ts",
"test:e2e:web3-adapter": "vitest src/e2e/evault-core.e2e.spec.ts --run --config vitest.config.e2e.ts",
"typeorm": "typeorm-ts-node-commonjs",
"migration:generate": "npm run typeorm migration:generate -- -d src/config/database.ts",
"migration:run": "npm run typeorm migration:run -- -d src/config/database.ts",
Expand Down
29 changes: 23 additions & 6 deletions infrastructure/evault-core/src/core/protocol/graphql-server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -196,6 +196,12 @@ export class GraphQLServer {
context.eName
);

// Add parsed field to metaEnvelope for GraphQL response
const metaEnvelopeWithParsed = {
...result.metaEnvelope,
parsed: input.payload,
};

// Deliver webhooks for create operation
const requestingPlatform =
context.tokenPayload?.platform || null;
Expand Down Expand Up @@ -224,7 +230,10 @@ export class GraphQLServer {
);
}, 3_000);

return result;
return {
...result,
metaEnvelope: metaEnvelopeWithParsed,
};
}
),
updateMetaEnvelopeById: this.accessGuard.middleware(
Expand Down Expand Up @@ -330,11 +339,19 @@ export class GraphQLServer {
const eName = request.headers.get("x-ename") ?? request.headers.get("X-ENAME") ?? null;

if (token) {
const id = getJWTHeader(token).kid?.split("#")[0];
return {
currentUser: id ?? null,
eName: eName,
};
try {
const id = getJWTHeader(token).kid?.split("#")[0];
return {
currentUser: id ?? null,
eName: eName,
};
} catch (error) {
// Invalid JWT token - ignore and continue without currentUser
return {
currentUser: null,
eName: eName,
};
}
}

return {
Expand Down
Copy link
Contributor

@xPathin xPathin Nov 7, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

A lot of "as any" usages in this file.

Original file line number Diff line number Diff line change
Expand Up @@ -136,7 +136,7 @@ describe("VaultAccessGuard", () => {
const checkAccess = (guard as any).checkAccess.bind(guard);
const result = await checkAccess("meta-envelope-id", context);

expect(result).toBe(true);
expect(result.hasAccess).toBe(true);
expect(context.tokenPayload).toBeDefined();
});

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

expect(result).toBe(true);
expect(result.hasAccess).toBe(true);
});

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

expect(result).toBe(true);
expect(result.hasAccess).toBe(true);
});

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

expect(result).toBe(false);
expect(result.hasAccess).toBe(false);
});

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

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

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

const result1 = await checkAccess(metaEnvelope1.metaEnvelope.id, context1);
expect(result1).toBe(true);
expect(result1.hasAccess).toBe(true);

// Tenant1 should NOT access tenant2's data
const result2 = await checkAccess(metaEnvelope2.metaEnvelope.id, context1);
expect(result2).toBe(false);
expect(result2.hasAccess).toBe(false);

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

const result3 = await checkAccess(metaEnvelope2.metaEnvelope.id, context2);
expect(result3).toBe(true);
expect(result3.hasAccess).toBe(true);

// Tenant2 should NOT access tenant1's data
const result4 = await checkAccess(metaEnvelope1.metaEnvelope.id, context2);
expect(result4).toBe(false);
expect(result4.hasAccess).toBe(false);
});

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

expect(result).toBe(true);
expect(result.hasAccess).toBe(true);
expect(result.exists).toBe(true);
});
});

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

const wrappedResolver = guard.middleware(mockResolver);

// Should throw "Access denied" because the meta-envelope won't be found with eName2
await expect(
wrappedResolver(null, { id: metaEnvelope.metaEnvelope.id }, context)
).rejects.toThrow("Access denied");
// When envelope doesn't exist (wrong eName), middleware returns null (not found)
const result = await wrappedResolver(null, { id: metaEnvelope.metaEnvelope.id }, context);
expect(result).toBeNull();
});
});
});
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -54,12 +54,12 @@ export class VaultAccessGuard {
* Checks if the current user has access to a meta envelope based on its ACL
* @param metaEnvelopeId - The ID of the meta envelope to check access for
* @param context - The GraphQL context containing the current user
* @returns Promise<boolean> - Whether the user has access
* @returns Promise<{hasAccess: boolean, exists: boolean}> - Whether the user has access and if envelope exists
*/
private async checkAccess(
metaEnvelopeId: string,
context: VaultContext
): Promise<boolean> {
): Promise<{ hasAccess: boolean; exists: boolean }> {
// Validate token if present
const authHeader =
context.request?.headers?.get("authorization") ??
Expand All @@ -69,36 +69,40 @@ export class VaultAccessGuard {
if (tokenPayload) {
// Token is valid, set platform context and allow access
context.tokenPayload = tokenPayload;
return true;
// Still need to check if envelope exists
if (!context.eName) {
return { hasAccess: true, exists: false };
}
const metaEnvelope = await this.db.findMetaEnvelopeById(metaEnvelopeId, context.eName);
return { hasAccess: true, exists: metaEnvelope !== null };
}

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

// Fallback to original ACL logic if no valid token
if (!context.currentUser) {
const metaEnvelope = await this.db.findMetaEnvelopeById(
metaEnvelopeId,
context.eName
);
if (metaEnvelope && metaEnvelope.acl.includes("*")) return true;
return false;
}

const metaEnvelope = await this.db.findMetaEnvelopeById(metaEnvelopeId, context.eName);
if (!metaEnvelope) {
return false;
return { hasAccess: false, exists: false };
}

// Fallback to original ACL logic if no valid token
if (!context.currentUser) {
if (metaEnvelope.acl.includes("*")) {
return { hasAccess: true, exists: true };
}
return { hasAccess: false, exists: true };
}

// If ACL contains "*", anyone can access
if (metaEnvelope.acl.includes("*")) {
return true;
return { hasAccess: true, exists: true };
}

// Check if the current user's ID is in the ACL
return metaEnvelope.acl.includes(context.currentUser);
const hasAccess = metaEnvelope.acl.includes(context.currentUser);
return { hasAccess, exists: true };
}

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

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

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

const hasAccess = await this.checkAccess(metaEnvelopeId, context);
// Check if envelope exists and user has access
const { hasAccess, exists } = await this.checkAccess(metaEnvelopeId, context);
if (!hasAccess) {
// If envelope doesn't exist, return null (not found)
if (!exists) {
return null;
}
// Envelope exists but access denied
throw new Error("Access denied");
}

// console.log
// Execute resolver and filter ACL
const result = await resolver(parent, args, context);

// If result is null (envelope not found), return null
if (result === null) {
return null;
}

return this.filterACL(result);
};
}
Expand Down
Loading