feat: v2 rate limiting (#16882)

* refactor: pass redis storage to throttler guard

* chore: upgrade throttler to latest

* feat: ApiKey RateLimit table

* chore: upgrade redis storage throttler

* feat: rate limit by api key

* refactor: on delete api key cascade rate limit

* fix: permissions guard work with oauth credentials

* chore: set rate limit in env

* tests: throttler

* feat: include rate limit name in response

* fix: correctly handle multiple rate limits

* chore: remove unused import

* delete migrations

* chore: prisma migration

* doc

* dummy

* fix: permissions guard unit test

* refactor: remove route specific @Throttles

* fix: permissions guard
This commit is contained in:
Lauris Skraucis
2024-10-01 12:01:56 +00:00
committed by GitHub
parent 9d3be8de03
commit 4b6a389212
15 changed files with 747 additions and 80 deletions
@@ -1,3 +1,4 @@
import { OAuthClientRepository } from "@/modules/oauth-clients/oauth-client.repository";
import { TokensRepository } from "@/modules/tokens/tokens.repository";
import { createMock } from "@golevelup/ts-jest";
import { ExecutionContext } from "@nestjs/common";
@@ -26,7 +27,8 @@ describe("PermissionsGuard", () => {
return null;
}
}),
})
}),
createMock<OAuthClientRepository>()
);
});
@@ -38,7 +40,7 @@ describe("PermissionsGuard", () => {
it("should return false", async () => {
const mockContext = createMockExecutionContext({});
jest.spyOn(reflector, "get").mockReturnValue([SCHEDULE_WRITE]);
jest.spyOn(guard, "getOAuthClientPermissions").mockResolvedValue(0);
jest.spyOn(guard, "getOAuthClientPermissionsByAccessToken").mockResolvedValue(0);
await expect(guard.canActivate(mockContext)).resolves.toBe(false);
});
@@ -51,7 +53,7 @@ describe("PermissionsGuard", () => {
let oAuthClientPermissions = 0;
oAuthClientPermissions |= SCHEDULE_WRITE;
jest.spyOn(guard, "getOAuthClientPermissions").mockResolvedValue(oAuthClientPermissions);
jest.spyOn(guard, "getOAuthClientPermissionsByAccessToken").mockResolvedValue(oAuthClientPermissions);
await expect(guard.canActivate(mockContext)).resolves.toBe(true);
});
@@ -62,7 +64,7 @@ describe("PermissionsGuard", () => {
let oAuthClientPermissions = 0;
oAuthClientPermissions |= SCHEDULE_WRITE;
oAuthClientPermissions |= SCHEDULE_READ;
jest.spyOn(guard, "getOAuthClientPermissions").mockResolvedValue(oAuthClientPermissions);
jest.spyOn(guard, "getOAuthClientPermissionsByAccessToken").mockResolvedValue(oAuthClientPermissions);
await expect(guard.canActivate(mockContext)).resolves.toBe(true);
});
@@ -73,7 +75,7 @@ describe("PermissionsGuard", () => {
let oAuthClientPermissions = 0;
oAuthClientPermissions |= SCHEDULE_WRITE;
jest.spyOn(guard, "getOAuthClientPermissions").mockResolvedValue(oAuthClientPermissions);
jest.spyOn(guard, "getOAuthClientPermissionsByAccessToken").mockResolvedValue(oAuthClientPermissions);
await expect(guard.canActivate(mockContext)).resolves.toBe(true);
});
@@ -83,7 +85,7 @@ describe("PermissionsGuard", () => {
let oAuthClientPermissions = 0;
oAuthClientPermissions |= APPS_WRITE;
jest.spyOn(guard, "getOAuthClientPermissions").mockResolvedValue(oAuthClientPermissions);
jest.spyOn(guard, "getOAuthClientPermissionsByAccessToken").mockResolvedValue(oAuthClientPermissions);
await expect(guard.canActivate(mockContext)).resolves.toBe(false);
});
@@ -94,7 +96,63 @@ describe("PermissionsGuard", () => {
let oAuthClientPermissions = 0;
oAuthClientPermissions |= SCHEDULE_WRITE;
jest.spyOn(guard, "getOAuthClientPermissions").mockResolvedValue(oAuthClientPermissions);
jest.spyOn(guard, "getOAuthClientPermissionsByAccessToken").mockResolvedValue(oAuthClientPermissions);
await expect(guard.canActivate(mockContext)).resolves.toBe(false);
});
});
describe("when oauth id is provided", () => {
it("should return true for valid permissions", async () => {
const mockContext = createMockExecutionContext({ "x-cal-client-id": "100" });
jest.spyOn(reflector, "get").mockReturnValue([SCHEDULE_WRITE]);
let oAuthClientPermissions = 0;
oAuthClientPermissions |= SCHEDULE_WRITE;
jest.spyOn(guard, "getOAuthClientPermissionsById").mockResolvedValue(oAuthClientPermissions);
await expect(guard.canActivate(mockContext)).resolves.toBe(true);
});
it("should return true for multiple valid permissions", async () => {
const mockContext = createMockExecutionContext({ "x-cal-client-id": "100" });
jest.spyOn(reflector, "get").mockReturnValue([SCHEDULE_WRITE, SCHEDULE_READ]);
let oAuthClientPermissions = 0;
oAuthClientPermissions |= SCHEDULE_WRITE;
oAuthClientPermissions |= SCHEDULE_READ;
jest.spyOn(guard, "getOAuthClientPermissionsById").mockResolvedValue(oAuthClientPermissions);
await expect(guard.canActivate(mockContext)).resolves.toBe(true);
});
it("should return true for empty Permissions decorator", async () => {
const mockContext = createMockExecutionContext({ "x-cal-client-id": "100" });
jest.spyOn(reflector, "get").mockReturnValue([]);
let oAuthClientPermissions = 0;
oAuthClientPermissions |= SCHEDULE_WRITE;
jest.spyOn(guard, "getOAuthClientPermissionsById").mockResolvedValue(oAuthClientPermissions);
await expect(guard.canActivate(mockContext)).resolves.toBe(true);
});
it("should return false for invalid permissions", async () => {
const mockContext = createMockExecutionContext({ "x-cal-client-id": "100" });
jest.spyOn(reflector, "get").mockReturnValue([SCHEDULE_WRITE]);
let oAuthClientPermissions = 0;
oAuthClientPermissions |= APPS_WRITE;
jest.spyOn(guard, "getOAuthClientPermissionsById").mockResolvedValue(oAuthClientPermissions);
await expect(guard.canActivate(mockContext)).resolves.toBe(false);
});
it("should return false for a missing permission", async () => {
const mockContext = createMockExecutionContext({ "x-cal-client-id": "100" });
jest.spyOn(reflector, "get").mockReturnValue([SCHEDULE_WRITE, SCHEDULE_READ]);
let oAuthClientPermissions = 0;
oAuthClientPermissions |= SCHEDULE_WRITE;
jest.spyOn(guard, "getOAuthClientPermissionsById").mockResolvedValue(oAuthClientPermissions);
await expect(guard.canActivate(mockContext)).resolves.toBe(false);
});
@@ -1,11 +1,13 @@
import { isApiKey } from "@/lib/api-key";
import { Permissions } from "@/modules/auth/decorators/permissions/permissions.decorator";
import { OAuthClientRepository } from "@/modules/oauth-clients/oauth-client.repository";
import { TokensRepository } from "@/modules/tokens/tokens.repository";
import { Injectable, CanActivate, ExecutionContext } from "@nestjs/common";
import { ConfigService } from "@nestjs/config";
import { Reflector } from "@nestjs/core";
import { getToken } from "next-auth/jwt";
import { X_CAL_CLIENT_ID } from "@calcom/platform-constants";
import { hasPermissions } from "@calcom/platform-utils";
@Injectable()
@@ -13,7 +15,8 @@ export class PermissionsGuard implements CanActivate {
constructor(
private reflector: Reflector,
private tokensRepository: TokensRepository,
private readonly config: ConfigService
private readonly config: ConfigService,
private readonly oAuthClientRepository: OAuthClientRepository
) {}
async canActivate(context: ExecutionContext): Promise<boolean> {
@@ -27,12 +30,13 @@ export class PermissionsGuard implements CanActivate {
const authString = request.get("Authorization")?.replace("Bearer ", "");
const nextAuthSecret = this.config.get("next.authSecret", { infer: true });
const nextAuthToken = await getToken({ req: request, secret: nextAuthSecret });
const oAuthClientId = request.params?.clientId || request.get(X_CAL_CLIENT_ID);
if (nextAuthToken) {
return true;
}
if (!authString) {
if (!authString && !oAuthClientId) {
return false;
}
@@ -41,7 +45,9 @@ export class PermissionsGuard implements CanActivate {
return true;
}
const oAuthClientPermissions = await this.getOAuthClientPermissions(authString);
const oAuthClientPermissions = authString
? await this.getOAuthClientPermissionsByAccessToken(authString)
: await this.getOAuthClientPermissionsById(oAuthClientId);
if (!oAuthClientPermissions) {
return false;
@@ -50,8 +56,13 @@ export class PermissionsGuard implements CanActivate {
return hasPermissions(oAuthClientPermissions, [...requiredPermissions]);
}
async getOAuthClientPermissions(accessToken: string) {
async getOAuthClientPermissionsByAccessToken(accessToken: string) {
const oAuthClient = await this.tokensRepository.getAccessTokenClient(accessToken);
return oAuthClient?.permissions;
}
async getOAuthClientPermissionsById(id: string) {
const oAuthClient = await this.oAuthClientRepository.getOAuthClient(id);
return oAuthClient?.permissions;
}
}