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
1 change: 1 addition & 0 deletions apps/api/v2/src/modules/auth/guards/or-guard/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export { Or } from "./or.guard";
104 changes: 104 additions & 0 deletions apps/api/v2/src/modules/auth/guards/or-guard/or.guard.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,104 @@
import { ExecutionContext, CanActivate } from "@nestjs/common";
Copy link
Member Author

Choose a reason for hiding this comment

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

No change in this file, when adding back


import { Or } from "./or.guard";

// Mock guards for testing
class MockGuard1 implements CanActivate {
constructor(private shouldPass: boolean) {}

async canActivate(): Promise<boolean> {
return this.shouldPass;
}
}

class MockGuard2 implements CanActivate {
constructor(private shouldPass: boolean) {}

async canActivate(): Promise<boolean> {
return this.shouldPass;
}
}

class MockGuard3 implements CanActivate {
constructor(private shouldThrow: boolean = false) {}

async canActivate(): Promise<boolean> {
if (this.shouldThrow) {
throw new Error("Guard failed");
}
return false;
}
}

describe("OrGuard", () => {
let guard: InstanceType<ReturnType<typeof Or>>;
let mockExecutionContext: ExecutionContext;
let mockModuleRef: any;

beforeEach(() => {
mockModuleRef = {
get: jest.fn(),
};

const OrGuardClass = Or([MockGuard1, MockGuard2]);
guard = new OrGuardClass(mockModuleRef);
mockExecutionContext = {} as ExecutionContext;
});

it("should be defined", () => {
expect(guard).toBeDefined();
});

it("should grant access when first guard passes", async () => {
const mockGuard1 = new MockGuard1(true);
const mockGuard2 = new MockGuard2(false);

mockModuleRef.get.mockReturnValueOnce(mockGuard1).mockReturnValueOnce(mockGuard2);

const result = await guard.canActivate(mockExecutionContext);

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

it("should grant access when second guard passes", async () => {
const mockGuard1 = new MockGuard1(false);
const mockGuard2 = new MockGuard2(true);

mockModuleRef.get.mockReturnValueOnce(mockGuard1).mockReturnValueOnce(mockGuard2);

const result = await guard.canActivate(mockExecutionContext);

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

it("should deny access when all guards fail", async () => {
const mockGuard1 = new MockGuard1(false);
const mockGuard2 = new MockGuard2(false);

mockModuleRef.get.mockReturnValueOnce(mockGuard1).mockReturnValueOnce(mockGuard2);

const result = await guard.canActivate(mockExecutionContext);

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

it("should continue checking other guards when one throws an error", async () => {
const mockGuard1 = new MockGuard3(true); // throws error
const mockGuard2 = new MockGuard2(true); // passes

mockModuleRef.get.mockReturnValueOnce(mockGuard1).mockReturnValueOnce(mockGuard2);

const result = await guard.canActivate(mockExecutionContext);

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

describe("Or decorator", () => {
it("should create a guard class with the specified guards", () => {
const OrGuardClass = Or([MockGuard1, MockGuard2]);

expect(OrGuardClass).toBeDefined();
expect(typeof OrGuardClass).toBe("function");
});
});
49 changes: 49 additions & 0 deletions apps/api/v2/src/modules/auth/guards/or-guard/or.guard.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
import { Injectable, CanActivate, ExecutionContext, Type, Logger } from "@nestjs/common";
Copy link
Member Author

Choose a reason for hiding this comment

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

No change in this file, when adding back

import { ModuleRef } from "@nestjs/core";

/**
* Decorator function that creates an Or guard with the specified guards
* @param guards Array of guard classes to evaluate with OR logic
* @returns A guard class that grants access if ANY of the provided guards return true
*/
export function Or(guards: Type<CanActivate>[]) {
@Injectable()
class OrGuard implements CanActivate {
public readonly logger = new Logger("OrGuard");

constructor(public readonly moduleRef: ModuleRef) {}

async canActivate(context: ExecutionContext): Promise<boolean> {
let lastError: unknown | null = null;
for (const Guard of guards) {
try {
const guardInstance = this.moduleRef.get(Guard, { strict: false });
const result = await Promise.resolve(guardInstance.canActivate(context));

if (result === true) {
this.logger.log(`OrGuard - Guard ${Guard.name} granted access`);
return true; // Access granted if any guard returns true
}
} catch (error) {
lastError = error;
// If a guard throws an exception, it implies failure for that specific guard.
// We catch it and continue checking other guards in the OR chain.
// If an exception should stop the entire chain immediately, re-throw it here.
this.logger.log(
`OrGuard - Guard ${Guard.name} failed: ${
error instanceof Error ? error.message : "Unknown error"
}`
);
}
}

this.logger.log("OrGuard - All guards failed, access denied");
if (lastError) {
throw lastError;
}
return false;
}
}

return OrGuard;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
import { ApiAuthGuardUser } from "@/modules/auth/strategies/api-auth/api-auth.strategy";
Copy link
Member Author

Choose a reason for hiding this comment

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

No change in this file, when adding back

import { PrismaReadService } from "@/modules/prisma/prisma-read.service";
import { Injectable, CanActivate, ExecutionContext, ForbiddenException } from "@nestjs/common";
import { Request } from "express";

import { Team } from "@calcom/prisma/client";

@Injectable()
export class IsUserRoutingForm implements CanActivate {
constructor(private readonly dbRead: PrismaReadService) {}

async canActivate(context: ExecutionContext): Promise<boolean> {
const request = context.switchToHttp().getRequest<Request & { organization: Team }>();
const routingFormId: string = request.params.routingFormId;
const user = request.user as ApiAuthGuardUser;
if (!routingFormId) {
throw new ForbiddenException("IsUserRoutingForm - No routing form id found in request params.");
}

const userRoutingForm = await this.dbRead.prisma.app_RoutingForms_Form.findFirst({
where: {
id: routingFormId,
userId: Number(user.id),
teamId: null,
},
select: {
id: true,
},
});

if (!userRoutingForm) {
throw new ForbiddenException(
`Routing Form with id=${routingFormId} is not a user Routing Form owned by user with id=${user.id}.`
);
}

return true;
}
}
Loading
Loading