feat: v2 managed organizations (#19341)
* refactor: allow non unique PlatformBilling customerId * feat: add PlatformBilling managed organizations fields * feat: ManagedOrganization table * refactor: bill overdue based on teamId * refactor: restructure organizations module * wip: organizations endpoints * Revert "wip: organizations endpoints" This reverts commit 0e9e66fc74f31da436f12930c1b6597c650ed621. * refactor: unique index for managed organizations * wip: organizations endpoints * wip: create managed organization * remove unecessary membership check because we have guard * feat: create managed org * feat: get managed org and orgs * feat: update and delete managed orgs * wip: api key logic * feat: allow variable api key length * feat: refresh managed org api key * feat: create managed org OAuth clients using api key * finish merge main * chore: bump platform libraries * tests: fix and add more tests to api-auth.strategy.e2e * refactor: dont request managed org slug and handle metadata as object * revert: billing service and repository update overdue based on sub and customer ids * refactor: v2 OAuth client permissions (#19501) * refactor: v2 permissions as string array * refactor: frontend work with permissions as string * tests: test '*' permissions * fix:managed org creator have profile & test can fetch oauth client * fix: tests * fix: OAuthClientCard on frontend * fix: tests
This commit is contained in:
@@ -1,4 +1,4 @@
|
||||
import { ApiKeyModule } from "@/modules/api-key/api-key.module";
|
||||
import { ApiKeysModule } from "@/modules/api-keys/api-keys.module";
|
||||
import { ApiAuthGuard } from "@/modules/auth/guards/api-auth/api-auth.guard";
|
||||
import { NextAuthGuard } from "@/modules/auth/guards/next-auth/next-auth.guard";
|
||||
import { ApiAuthStrategy } from "@/modules/auth/strategies/api-auth/api-auth.strategy";
|
||||
@@ -17,7 +17,7 @@ import { PassportModule } from "@nestjs/passport";
|
||||
imports: [
|
||||
PassportModule,
|
||||
RedisModule,
|
||||
ApiKeyModule,
|
||||
ApiKeysModule,
|
||||
UsersModule,
|
||||
MembershipsModule,
|
||||
TokensModule,
|
||||
|
||||
@@ -0,0 +1,12 @@
|
||||
import { createParamDecorator, ExecutionContext, UnauthorizedException } from "@nestjs/common";
|
||||
|
||||
export const GetOrgId = createParamDecorator((_: unknown, ctx: ExecutionContext): number => {
|
||||
const request = ctx.switchToHttp().getRequest();
|
||||
const organizationId = request.organizationId;
|
||||
|
||||
if (!organizationId) {
|
||||
throw new UnauthorizedException("GetOrgId decorator: Organization ID not found");
|
||||
}
|
||||
|
||||
return organizationId;
|
||||
});
|
||||
@@ -1,5 +1,5 @@
|
||||
import { PlatformPlanGuard } from "@/modules/auth/guards/billing/platform-plan.guard";
|
||||
import { OrganizationsRepository } from "@/modules/organizations/organizations.repository";
|
||||
import { OrganizationsRepository } from "@/modules/organizations/index/organizations.repository";
|
||||
import { RedisService } from "@/modules/redis/redis.service";
|
||||
import { createMock } from "@golevelup/ts-jest";
|
||||
import { ExecutionContext } from "@nestjs/common";
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { PlatformPlan } from "@/modules/auth/decorators/billing/platform-plan.decorator";
|
||||
import { ApiAuthGuardUser } from "@/modules/auth/strategies/api-auth/api-auth.strategy";
|
||||
import { PlatformPlanType } from "@/modules/billing/types";
|
||||
import { OrganizationsRepository } from "@/modules/organizations/organizations.repository";
|
||||
import { OrganizationsRepository } from "@/modules/organizations/index/organizations.repository";
|
||||
import { RedisService } from "@/modules/redis/redis.service";
|
||||
import { Injectable, CanActivate, ExecutionContext } from "@nestjs/common";
|
||||
import { Reflector } from "@nestjs/core";
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { OrganizationsMembershipRepository } from "@/modules/organizations/repositories/organizations-membership.repository";
|
||||
import { OrganizationsMembershipRepository } from "@/modules/organizations/memberships/organizations-membership.repository";
|
||||
import {
|
||||
Injectable,
|
||||
CanActivate,
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { MembershipRoles } from "@/modules/auth/decorators/roles/membership-roles.decorator";
|
||||
import { MembershipsRepository } from "@/modules/memberships/memberships.repository";
|
||||
import { OrganizationsService } from "@/modules/organizations/services/organizations.service";
|
||||
import { OrganizationsService } from "@/modules/organizations/index/organizations.service";
|
||||
import { UsersService } from "@/modules/users/services/users.service";
|
||||
import { UserWithProfile } from "@/modules/users/users.repository";
|
||||
import { Injectable, CanActivate, ExecutionContext, ForbiddenException } from "@nestjs/common";
|
||||
@@ -20,10 +20,10 @@ export class OrganizationRolesGuard implements CanActivate {
|
||||
async canActivate(context: ExecutionContext): Promise<boolean> {
|
||||
const request = context.switchToHttp().getRequest();
|
||||
const user: UserWithProfile = request.user;
|
||||
const organizationId = user ? this.usersService.getUserMainOrgId(user) : null;
|
||||
const organizationId = this.getOrganizationId(context);
|
||||
|
||||
if (!user || !organizationId) {
|
||||
throw new ForbiddenException("No organization associated with the user.");
|
||||
throw new ForbiddenException("OrganizationRolesGuard - No organization associated with the user.");
|
||||
}
|
||||
|
||||
await this.isPlatform(organizationId);
|
||||
@@ -40,13 +40,25 @@ export class OrganizationRolesGuard implements CanActivate {
|
||||
async isPlatform(organizationId: number) {
|
||||
const isPlatform = await this.organizationsService.isPlatform(organizationId);
|
||||
if (!isPlatform) {
|
||||
throw new ForbiddenException("Organization is not a platform (SHP).");
|
||||
throw new ForbiddenException("OrganizationRolesGuard - Organization is not a platform (SHP).");
|
||||
}
|
||||
}
|
||||
|
||||
getOrganizationId(context: ExecutionContext) {
|
||||
const request = context.switchToHttp().getRequest();
|
||||
const user: UserWithProfile = request.user;
|
||||
const authMethodOrganizationId = request.organizationId;
|
||||
if (authMethodOrganizationId) return authMethodOrganizationId;
|
||||
|
||||
const userOrganizationId = user ? this.usersService.getUserMainOrgId(user) : null;
|
||||
return userOrganizationId;
|
||||
}
|
||||
|
||||
isMembershipAccepted(accepted: boolean) {
|
||||
if (!accepted) {
|
||||
throw new ForbiddenException(`User has not accepted membership in the organization.`);
|
||||
throw new ForbiddenException(
|
||||
`OrganizationRolesGuard - User has not accepted membership in the organization.`
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -57,7 +69,9 @@ export class OrganizationRolesGuard implements CanActivate {
|
||||
|
||||
const hasRequiredRole = allowedRoles.includes(membershipRole);
|
||||
if (!hasRequiredRole) {
|
||||
throw new ForbiddenException(`User must have one of the roles: ${allowedRoles.join(", ")}.`);
|
||||
throw new ForbiddenException(
|
||||
`OrganizationRolesGuard - User must have one of the roles: ${allowedRoles.join(", ")}.`
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { OrganizationsRepository } from "@/modules/organizations/organizations.repository";
|
||||
import { OrganizationsRepository } from "@/modules/organizations/index/organizations.repository";
|
||||
import { RedisService } from "@/modules/redis/redis.service";
|
||||
import { Injectable, CanActivate, ExecutionContext, ForbiddenException } from "@nestjs/common";
|
||||
import { Request } from "express";
|
||||
|
||||
+43
@@ -0,0 +1,43 @@
|
||||
import { ManagedOrganizationsRepository } from "@/modules/organizations/organizations/managed-organizations.repository";
|
||||
import {
|
||||
Injectable,
|
||||
CanActivate,
|
||||
ExecutionContext,
|
||||
ForbiddenException,
|
||||
NotFoundException,
|
||||
} from "@nestjs/common";
|
||||
import { Request } from "express";
|
||||
|
||||
import { Team } from "@calcom/prisma/client";
|
||||
|
||||
@Injectable()
|
||||
export class IsManagedOrgInManagerOrg implements CanActivate {
|
||||
constructor(private managedOrganizationsRepository: ManagedOrganizationsRepository) {}
|
||||
|
||||
async canActivate(context: ExecutionContext): Promise<boolean> {
|
||||
const request = context.switchToHttp().getRequest<Request & { managedOrganization: Team }>();
|
||||
const managedOrgId: string = request.params.managedOrganizationId;
|
||||
const managerOrgId: string = request.params.orgId;
|
||||
|
||||
if (!managerOrgId) {
|
||||
throw new ForbiddenException("No manager org id found in request params.");
|
||||
}
|
||||
|
||||
if (!managedOrgId) {
|
||||
throw new ForbiddenException("No managed org id found in request params.");
|
||||
}
|
||||
|
||||
const managedOrganization = await this.managedOrganizationsRepository.getByManagerManagedIds(
|
||||
Number(managerOrgId),
|
||||
Number(managedOrgId)
|
||||
);
|
||||
|
||||
if (!managedOrganization) {
|
||||
throw new NotFoundException(
|
||||
`Managed organization with id=${managedOrgId} within manager organization with id=${managerOrgId} not found.`
|
||||
);
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
import { OrganizationsRepository } from "@/modules/organizations/organizations.repository";
|
||||
import { OrganizationsRepository } from "@/modules/organizations/index/organizations.repository";
|
||||
import { RedisService } from "@/modules/redis/redis.service";
|
||||
import { Injectable, CanActivate, ExecutionContext, ForbiddenException } from "@nestjs/common";
|
||||
import { Request } from "express";
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { OrganizationsRepository } from "@/modules/organizations/organizations.repository";
|
||||
import { OrganizationsWebhooksRepository } from "@/modules/organizations/repositories/organizations-webhooks.repository";
|
||||
import { OrganizationsRepository } from "@/modules/organizations/index/organizations.repository";
|
||||
import { OrganizationsWebhooksRepository } from "@/modules/organizations/webhooks/organizations-webhooks.repository";
|
||||
import { RedisService } from "@/modules/redis/redis.service";
|
||||
import { Injectable, CanActivate, ExecutionContext, ForbiddenException } from "@nestjs/common";
|
||||
import { Request } from "express";
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { OrganizationsTeamsRepository } from "@/modules/organizations/repositories/organizations-teams.repository";
|
||||
import { OrganizationsTeamsRepository } from "@/modules/organizations/teams/index/organizations-teams.repository";
|
||||
import {
|
||||
Injectable,
|
||||
CanActivate,
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { OrganizationsRepository } from "@/modules/organizations/organizations.repository";
|
||||
import { OrganizationsRepository } from "@/modules/organizations/index/organizations.repository";
|
||||
import { Injectable, CanActivate, ExecutionContext, ForbiddenException } from "@nestjs/common";
|
||||
import { Request } from "express";
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { OrganizationsRepository } from "@/modules/organizations/organizations.repository";
|
||||
import { OrganizationsRepository } from "@/modules/organizations/index/organizations.repository";
|
||||
import { Injectable, CanActivate, ExecutionContext, ForbiddenException } from "@nestjs/common";
|
||||
import { Request } from "express";
|
||||
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import appConfig from "@/config/app";
|
||||
import { ApiKeyRepository } from "@/modules/api-key/api-key-repository";
|
||||
import { AuthMethods } from "@/lib/enums/auth-methods";
|
||||
import { ApiKeysRepository } from "@/modules/api-keys/api-keys-repository";
|
||||
import { DeploymentsRepository } from "@/modules/deployments/deployments.repository";
|
||||
import { DeploymentsService } from "@/modules/deployments/deployments.service";
|
||||
import { JwtService } from "@/modules/jwt/jwt.service";
|
||||
@@ -9,6 +10,7 @@ import { PrismaReadService } from "@/modules/prisma/prisma-read.service";
|
||||
import { PrismaWriteService } from "@/modules/prisma/prisma-write.service";
|
||||
import { ProfilesModule } from "@/modules/profiles/profiles.module";
|
||||
import { TokensRepository } from "@/modules/tokens/tokens.repository";
|
||||
import { UsersService } from "@/modules/users/services/users.service";
|
||||
import { UsersRepository } from "@/modules/users/users.repository";
|
||||
import { ExecutionContext, HttpException } from "@nestjs/common";
|
||||
import { ConfigService } from "@nestjs/config";
|
||||
@@ -16,6 +18,7 @@ import { ConfigModule } from "@nestjs/config";
|
||||
import { JwtService as NestJwtService } from "@nestjs/jwt";
|
||||
import { Test, TestingModule } from "@nestjs/testing";
|
||||
import { PlatformOAuthClient, Team, User } from "@prisma/client";
|
||||
import { createRequest } from "node-mocks-http";
|
||||
import { ApiKeysRepositoryFixture } from "test/fixtures/repository/api-keys.repository.fixture";
|
||||
import { OAuthClientRepositoryFixture } from "test/fixtures/repository/oauth-client.repository.fixture";
|
||||
import { ProfileRepositoryFixture } from "test/fixtures/repository/profiles.repository.fixture";
|
||||
@@ -27,6 +30,7 @@ import { randomString } from "test/utils/randomString";
|
||||
|
||||
import { X_CAL_CLIENT_ID, X_CAL_SECRET_KEY } from "@calcom/platform-constants";
|
||||
|
||||
import { ApiAuthGuardRequest } from "./api-auth.strategy";
|
||||
import { ApiAuthStrategy } from "./api-auth.strategy";
|
||||
|
||||
describe("ApiAuthStrategy", () => {
|
||||
@@ -35,7 +39,9 @@ describe("ApiAuthStrategy", () => {
|
||||
let tokensRepositoryFixture: TokensRepositoryFixture;
|
||||
let teamRepositoryFixture: TeamRepositoryFixture;
|
||||
let organization: Team;
|
||||
let organizationTwo: Team;
|
||||
let oAuthClient: PlatformOAuthClient;
|
||||
let oAuthClientTwo: PlatformOAuthClient;
|
||||
let apiKeysRepositoryFixture: ApiKeysRepositoryFixture;
|
||||
let oAuthClientRepositoryFixture: OAuthClientRepositoryFixture;
|
||||
let profilesRepositoryFixture: ProfileRepositoryFixture;
|
||||
@@ -65,7 +71,8 @@ describe("ApiAuthStrategy", () => {
|
||||
ConfigService,
|
||||
OAuthFlowService,
|
||||
UsersRepository,
|
||||
ApiKeyRepository,
|
||||
UsersService,
|
||||
ApiKeysRepository,
|
||||
DeploymentsService,
|
||||
OAuthClientRepository,
|
||||
PrismaReadService,
|
||||
@@ -84,7 +91,11 @@ describe("ApiAuthStrategy", () => {
|
||||
teamRepositoryFixture = new TeamRepositoryFixture(module);
|
||||
oAuthClientRepositoryFixture = new OAuthClientRepositoryFixture(module);
|
||||
profilesRepositoryFixture = new ProfileRepositoryFixture(module);
|
||||
organization = await teamRepositoryFixture.create({ name: `api-auth-organization-${randomString()}` });
|
||||
organization = await teamRepositoryFixture.create({ name: `api-auth-organization-1-${randomString()}` });
|
||||
organizationTwo = await teamRepositoryFixture.create({
|
||||
name: `api-auth-organization-2-${randomString()}`,
|
||||
});
|
||||
|
||||
validApiKeyUser = await userRepositoryFixture.create({
|
||||
email: validApiKeyEmail,
|
||||
});
|
||||
@@ -103,6 +114,13 @@ describe("ApiAuthStrategy", () => {
|
||||
organization: { connect: { id: organization.id } },
|
||||
});
|
||||
|
||||
await profilesRepositoryFixture.create({
|
||||
uid: "asd-asd",
|
||||
username: validOAuthEmail,
|
||||
user: { connect: { id: validOAuthUser.id } },
|
||||
organization: { connect: { id: organizationTwo.id } },
|
||||
});
|
||||
|
||||
const data = {
|
||||
logo: "logo-url",
|
||||
name: "name",
|
||||
@@ -110,6 +128,7 @@ describe("ApiAuthStrategy", () => {
|
||||
permissions: 32,
|
||||
};
|
||||
oAuthClient = await oAuthClientRepositoryFixture.create(organization.id, data, "secret");
|
||||
oAuthClientTwo = await oAuthClientRepositoryFixture.create(organizationTwo.id, data, "secret");
|
||||
});
|
||||
|
||||
describe("authenticate with strategy", () => {
|
||||
@@ -119,25 +138,68 @@ describe("ApiAuthStrategy", () => {
|
||||
oAuthClient.id
|
||||
);
|
||||
|
||||
const user = await strategy.accessTokenStrategy(accessToken);
|
||||
const mockRequest = createRequest() as ApiAuthGuardRequest;
|
||||
mockRequest.authMethod = AuthMethods.ACCESS_TOKEN;
|
||||
mockRequest.organizationId = null;
|
||||
const user = await strategy.accessTokenStrategy(accessToken, mockRequest);
|
||||
expect(user).toBeDefined();
|
||||
expect(user?.id).toEqual(validAccessTokenUser.id);
|
||||
});
|
||||
|
||||
it("should return user associated with valid api key", async () => {
|
||||
it("should return user associated with valid api key and correctly set organizationId for org1", async () => {
|
||||
const now = new Date();
|
||||
now.setDate(now.getDate() + 1);
|
||||
const { keyString } = await apiKeysRepositoryFixture.createApiKey(validApiKeyUser.id, now);
|
||||
const { keyString } = await apiKeysRepositoryFixture.createApiKey(
|
||||
validApiKeyUser.id,
|
||||
now,
|
||||
organization.id
|
||||
);
|
||||
|
||||
const user = await strategy.apiKeyStrategy(keyString);
|
||||
const mockRequest = createRequest() as ApiAuthGuardRequest;
|
||||
mockRequest.authMethod = AuthMethods.API_KEY;
|
||||
mockRequest.organizationId = null;
|
||||
const user = await strategy.apiKeyStrategy(keyString, mockRequest);
|
||||
expect(user).toBeDefined();
|
||||
expect(user?.id).toEqual(validApiKeyUser.id);
|
||||
expect(mockRequest.organizationId).toEqual(organization.id);
|
||||
});
|
||||
|
||||
it("should return user associated with valid api key and correctly set organizationId for org2", async () => {
|
||||
const now = new Date();
|
||||
now.setDate(now.getDate() + 1);
|
||||
const { keyString } = await apiKeysRepositoryFixture.createApiKey(
|
||||
validApiKeyUser.id,
|
||||
now,
|
||||
organizationTwo.id
|
||||
);
|
||||
|
||||
const mockRequest = createRequest() as ApiAuthGuardRequest;
|
||||
mockRequest.authMethod = AuthMethods.API_KEY;
|
||||
mockRequest.organizationId = null;
|
||||
const user = await strategy.apiKeyStrategy(keyString, mockRequest);
|
||||
expect(user).toBeDefined();
|
||||
expect(user?.id).toEqual(validApiKeyUser.id);
|
||||
expect(mockRequest.organizationId).toEqual(organizationTwo.id);
|
||||
});
|
||||
|
||||
it("should return user associated with valid OAuth client", async () => {
|
||||
const user = await strategy.oAuthClientStrategy(oAuthClient.id, oAuthClient.secret);
|
||||
const mockRequest = createRequest() as ApiAuthGuardRequest;
|
||||
mockRequest.authMethod = AuthMethods.OAUTH_CLIENT;
|
||||
mockRequest.organizationId = null;
|
||||
const user = await strategy.oAuthClientStrategy(oAuthClient.id, oAuthClient.secret, mockRequest);
|
||||
expect(user).toBeDefined();
|
||||
expect(user.id).toEqual(validOAuthUser.id);
|
||||
expect(mockRequest.organizationId).toEqual(organization.id);
|
||||
});
|
||||
|
||||
it("should return user associated with valid OAuth client for org2", async () => {
|
||||
const mockRequest = createRequest() as ApiAuthGuardRequest;
|
||||
mockRequest.authMethod = AuthMethods.OAUTH_CLIENT;
|
||||
mockRequest.organizationId = null;
|
||||
const user = await strategy.oAuthClientStrategy(oAuthClientTwo.id, oAuthClientTwo.secret, mockRequest);
|
||||
expect(user).toBeDefined();
|
||||
expect(user.id).toEqual(validOAuthUser.id);
|
||||
expect(mockRequest.organizationId).toEqual(organizationTwo.id);
|
||||
});
|
||||
|
||||
it("should throw 401 if api key is invalid", async () => {
|
||||
|
||||
@@ -2,12 +2,13 @@ import { hashAPIKey, isApiKey, stripApiKey } from "@/lib/api-key";
|
||||
import { AuthMethods } from "@/lib/enums/auth-methods";
|
||||
import { isOriginAllowed } from "@/lib/is-origin-allowed/is-origin-allowed";
|
||||
import { BaseStrategy } from "@/lib/passport/strategies/types";
|
||||
import { ApiKeyRepository } from "@/modules/api-key/api-key-repository";
|
||||
import { ApiKeysRepository } from "@/modules/api-keys/api-keys-repository";
|
||||
import { DeploymentsService } from "@/modules/deployments/deployments.service";
|
||||
import { OAuthClientRepository } from "@/modules/oauth-clients/oauth-client.repository";
|
||||
import { OAuthFlowService } from "@/modules/oauth-clients/services/oauth-flow.service";
|
||||
import { ProfilesRepository } from "@/modules/profiles/profiles.repository";
|
||||
import { TokensRepository } from "@/modules/tokens/tokens.repository";
|
||||
import { UsersService } from "@/modules/users/services/users.service";
|
||||
import { UserWithProfile, UsersRepository } from "@/modules/users/users.repository";
|
||||
import { Injectable, InternalServerErrorException, UnauthorizedException } from "@nestjs/common";
|
||||
import { ConfigService } from "@nestjs/config";
|
||||
@@ -18,7 +19,11 @@ import { getToken } from "next-auth/jwt";
|
||||
import { INVALID_ACCESS_TOKEN, X_CAL_CLIENT_ID, X_CAL_SECRET_KEY } from "@calcom/platform-constants";
|
||||
|
||||
export type ApiAuthGuardUser = UserWithProfile & { isSystemAdmin: boolean };
|
||||
|
||||
export type ApiAuthGuardRequest = Request & {
|
||||
authMethod: AuthMethods;
|
||||
organizationId: number | null;
|
||||
user: ApiAuthGuardUser;
|
||||
};
|
||||
export const NO_AUTH_PROVIDED_MESSAGE =
|
||||
"No authentication method provided. Either pass an API key as 'Bearer' header or OAuth client credentials as 'x-cal-secret-key' and 'x-cal-client-id' headers";
|
||||
@Injectable()
|
||||
@@ -29,14 +34,15 @@ export class ApiAuthStrategy extends PassportStrategy(BaseStrategy, "api-auth")
|
||||
private readonly oauthFlowService: OAuthFlowService,
|
||||
private readonly tokensRepository: TokensRepository,
|
||||
private readonly userRepository: UsersRepository,
|
||||
private readonly apiKeyRepository: ApiKeyRepository,
|
||||
private readonly apiKeyRepository: ApiKeysRepository,
|
||||
private readonly oauthRepository: OAuthClientRepository,
|
||||
private readonly profilesRepository: ProfilesRepository
|
||||
private readonly profilesRepository: ProfilesRepository,
|
||||
private readonly usersService: UsersService
|
||||
) {
|
||||
super();
|
||||
}
|
||||
|
||||
async authenticate(request: Request & { authMethod: AuthMethods }) {
|
||||
async authenticate(request: ApiAuthGuardRequest) {
|
||||
try {
|
||||
const { params } = request;
|
||||
const oAuthClientSecret = request.get(X_CAL_SECRET_KEY);
|
||||
@@ -45,7 +51,7 @@ export class ApiAuthStrategy extends PassportStrategy(BaseStrategy, "api-auth")
|
||||
|
||||
if (oAuthClientId && oAuthClientSecret) {
|
||||
request.authMethod = AuthMethods["OAUTH_CLIENT"];
|
||||
return await this.authenticateOAuthClient(oAuthClientId, oAuthClientSecret);
|
||||
return await this.authenticateOAuthClient(oAuthClientId, oAuthClientSecret, request);
|
||||
}
|
||||
|
||||
if (bearerToken) {
|
||||
@@ -53,7 +59,7 @@ export class ApiAuthStrategy extends PassportStrategy(BaseStrategy, "api-auth")
|
||||
request.authMethod = isApiKey(bearerToken, this.config.get<string>("api.apiKeyPrefix") ?? "cal_")
|
||||
? AuthMethods["API_KEY"]
|
||||
: AuthMethods["ACCESS_TOKEN"];
|
||||
return await this.authenticateBearerToken(bearerToken, requestOrigin);
|
||||
return await this.authenticateBearerToken(bearerToken, request, requestOrigin);
|
||||
}
|
||||
|
||||
const nextAuthSecret = this.config.get("next.authSecret", { infer: true });
|
||||
@@ -61,7 +67,7 @@ export class ApiAuthStrategy extends PassportStrategy(BaseStrategy, "api-auth")
|
||||
|
||||
if (nextAuthToken) {
|
||||
request.authMethod = AuthMethods["NEXT_AUTH"];
|
||||
return await this.authenticateNextAuth(nextAuthToken);
|
||||
return await this.authenticateNextAuth(nextAuthToken, request);
|
||||
}
|
||||
|
||||
throw new UnauthorizedException(NO_AUTH_PROVIDED_MESSAGE);
|
||||
@@ -75,8 +81,8 @@ export class ApiAuthStrategy extends PassportStrategy(BaseStrategy, "api-auth")
|
||||
}
|
||||
}
|
||||
|
||||
async authenticateNextAuth(token: { email?: string | null }) {
|
||||
const user = await this.nextAuthStrategy(token);
|
||||
async authenticateNextAuth(token: { email?: string | null }, request: ApiAuthGuardRequest) {
|
||||
const user = await this.nextAuthStrategy(token, request);
|
||||
return this.success(this.getSuccessUser(user));
|
||||
}
|
||||
|
||||
@@ -87,12 +93,16 @@ export class ApiAuthStrategy extends PassportStrategy(BaseStrategy, "api-auth")
|
||||
};
|
||||
}
|
||||
|
||||
async authenticateOAuthClient(oAuthClientId: string, oAuthClientSecret: string) {
|
||||
const user = await this.oAuthClientStrategy(oAuthClientId, oAuthClientSecret);
|
||||
async authenticateOAuthClient(
|
||||
oAuthClientId: string,
|
||||
oAuthClientSecret: string,
|
||||
request: ApiAuthGuardRequest
|
||||
) {
|
||||
const user = await this.oAuthClientStrategy(oAuthClientId, oAuthClientSecret, request);
|
||||
return this.success(this.getSuccessUser(user));
|
||||
}
|
||||
|
||||
async oAuthClientStrategy(oAuthClientId: string, oAuthClientSecret: string) {
|
||||
async oAuthClientStrategy(oAuthClientId: string, oAuthClientSecret: string, request: ApiAuthGuardRequest) {
|
||||
const client = await this.oauthRepository.getOAuthClient(oAuthClientId);
|
||||
|
||||
if (!client) {
|
||||
@@ -115,14 +125,20 @@ export class ApiAuthStrategy extends PassportStrategy(BaseStrategy, "api-auth")
|
||||
throw new UnauthorizedException("No user associated with the provided OAuth client");
|
||||
}
|
||||
|
||||
request.organizationId = client.organizationId;
|
||||
|
||||
return user;
|
||||
}
|
||||
|
||||
async authenticateBearerToken(authString: string, requestOrigin: string | undefined) {
|
||||
async authenticateBearerToken(
|
||||
authString: string,
|
||||
request: ApiAuthGuardRequest,
|
||||
requestOrigin: string | undefined
|
||||
) {
|
||||
try {
|
||||
const user = isApiKey(authString, this.config.get<string>("api.apiKeyPrefix") ?? "cal_")
|
||||
? await this.apiKeyStrategy(authString)
|
||||
: await this.accessTokenStrategy(authString, requestOrigin);
|
||||
? await this.apiKeyStrategy(authString, request)
|
||||
: await this.accessTokenStrategy(authString, request, requestOrigin);
|
||||
|
||||
if (!user) {
|
||||
return this.error(new UnauthorizedException("No user associated with the provided token"));
|
||||
@@ -139,7 +155,7 @@ export class ApiAuthStrategy extends PassportStrategy(BaseStrategy, "api-auth")
|
||||
}
|
||||
}
|
||||
|
||||
async apiKeyStrategy(apiKey: string) {
|
||||
async apiKeyStrategy(apiKey: string, request: ApiAuthGuardRequest) {
|
||||
const isLicenseValid = await this.deploymentsService.checkLicense();
|
||||
if (!isLicenseValid) {
|
||||
throw new UnauthorizedException("Invalid or missing CALCOM_LICENSE_KEY environment variable");
|
||||
@@ -163,10 +179,12 @@ export class ApiAuthStrategy extends PassportStrategy(BaseStrategy, "api-auth")
|
||||
}
|
||||
|
||||
const user: UserWithProfile | null = await this.userRepository.findByIdWithProfile(apiKeyOwnerId);
|
||||
request.organizationId = keyData.teamId;
|
||||
|
||||
return user;
|
||||
}
|
||||
|
||||
async accessTokenStrategy(accessToken: string, origin?: string) {
|
||||
async accessTokenStrategy(accessToken: string, request: ApiAuthGuardRequest, origin?: string) {
|
||||
const accessTokenValid = await this.oauthFlowService.validateAccessToken(accessToken);
|
||||
if (!accessTokenValid) {
|
||||
throw new UnauthorizedException(INVALID_ACCESS_TOKEN);
|
||||
@@ -190,10 +208,17 @@ export class ApiAuthStrategy extends PassportStrategy(BaseStrategy, "api-auth")
|
||||
}
|
||||
|
||||
const user: UserWithProfile | null = await this.userRepository.findByIdWithProfile(ownerId);
|
||||
if (!user) {
|
||||
throw new UnauthorizedException("User associated with the authentication api key not found.");
|
||||
}
|
||||
|
||||
const organizationId = this.usersService.getUserMainOrgId(user) as number;
|
||||
request.organizationId = organizationId;
|
||||
|
||||
return user;
|
||||
}
|
||||
|
||||
async nextAuthStrategy(token: { email?: string | null }) {
|
||||
async nextAuthStrategy(token: { email?: string | null }, request: ApiAuthGuardRequest) {
|
||||
if (!token.email) {
|
||||
throw new UnauthorizedException("Email not found in the authentication token.");
|
||||
}
|
||||
@@ -202,6 +227,8 @@ export class ApiAuthStrategy extends PassportStrategy(BaseStrategy, "api-auth")
|
||||
if (!user) {
|
||||
throw new UnauthorizedException("User associated with the authentication token email not found.");
|
||||
}
|
||||
const organizationId = this.usersService.getUserMainOrgId(user) as number;
|
||||
request.organizationId = organizationId;
|
||||
|
||||
return user;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user