From 99cd0c5c416bb99e73cdd788f09aa49c2f0e6daf Mon Sep 17 00:00:00 2001 From: Sahitya Chandra Date: Fri, 22 Aug 2025 14:14:28 +0530 Subject: [PATCH] fix: Use Memberships with OWNER role for platform owner lookup (#22475) * fix platform owner identification to use Memberships with OWNER role * replaced the hardcoded OWNER string with the enum value * replaced enum with hard coded OWNER value for safty * added enum usage * added admin lookout if owner validation fails * chore * chore * fixed tests * chore * chore * chore * chore --------- Co-authored-by: Devanshu Sharma Co-authored-by: Kartik Saini <41051387+kart1ka@users.noreply.github.com> Co-authored-by: Rajiv Sahal Co-authored-by: Anik Dhabal Babu <81948346+anikdhabal@users.noreply.github.com> --- apps/api/v2/src/app.e2e-spec.ts | 10 +++++++ apps/api/v2/src/modules/auth/auth.module.ts | 2 -- .../api-auth/api-auth.strategy.e2e-spec.ts | 19 ++++++++++++ .../strategies/api-auth/api-auth.strategy.ts | 10 ++++--- .../memberships/memberships.repository.ts | 30 +++++++++++++++++++ .../oauth-flow.controller.e2e-spec.ts | 10 +++++++ 6 files changed, 75 insertions(+), 6 deletions(-) diff --git a/apps/api/v2/src/app.e2e-spec.ts b/apps/api/v2/src/app.e2e-spec.ts index 18a23020db..88c1874d9d 100644 --- a/apps/api/v2/src/app.e2e-spec.ts +++ b/apps/api/v2/src/app.e2e-spec.ts @@ -9,6 +9,7 @@ import { TestingModule } from "@nestjs/testing"; import { Test } from "@nestjs/testing"; import * as request from "supertest"; import { ApiKeysRepositoryFixture } from "test/fixtures/repository/api-keys.repository.fixture"; +import { MembershipRepositoryFixture } from "test/fixtures/repository/membership.repository.fixture"; import { OAuthClientRepositoryFixture } from "test/fixtures/repository/oauth-client.repository.fixture"; import { OrganizationRepositoryFixture } from "test/fixtures/repository/organization.repository.fixture"; import { ProfileRepositoryFixture } from "test/fixtures/repository/profiles.repository.fixture"; @@ -33,6 +34,7 @@ describe("AppController", () => { let organizationsRepositoryFixture: OrganizationRepositoryFixture; let oauthClientRepositoryFixture: OAuthClientRepositoryFixture; let profilesRepositoryFixture: ProfileRepositoryFixture; + let membershipRepositoryFixture: MembershipRepositoryFixture; let apiKeyString: string; @@ -108,6 +110,14 @@ describe("AppController", () => { organization: { connect: { id: organization.id } }, }); + membershipRepositoryFixture = new MembershipRepositoryFixture(moduleRef); + await membershipRepositoryFixture.create({ + user: { connect: { id: user.id } }, + team: { connect: { id: organization.id } }, + role: "OWNER", + accepted: true, + }); + app = moduleRef.createNestApplication(); await app.init(); }); diff --git a/apps/api/v2/src/modules/auth/auth.module.ts b/apps/api/v2/src/modules/auth/auth.module.ts index eea576c84e..649565f6aa 100644 --- a/apps/api/v2/src/modules/auth/auth.module.ts +++ b/apps/api/v2/src/modules/auth/auth.module.ts @@ -6,7 +6,6 @@ import { NextAuthStrategy } from "@/modules/auth/strategies/next-auth/next-auth. import { DeploymentsModule } from "@/modules/deployments/deployments.module"; import { MembershipsModule } from "@/modules/memberships/memberships.module"; import { OAuthFlowService } from "@/modules/oauth-clients/services/oauth-flow.service"; -import { ProfilesModule } from "@/modules/profiles/profiles.module"; import { RedisModule } from "@/modules/redis/redis.module"; import { TokensModule } from "@/modules/tokens/tokens.module"; import { UsersModule } from "@/modules/users/users.module"; @@ -22,7 +21,6 @@ import { PassportModule } from "@nestjs/passport"; MembershipsModule, TokensModule, DeploymentsModule, - ProfilesModule, ], providers: [NextAuthGuard, NextAuthStrategy, ApiAuthGuard, ApiAuthStrategy, OAuthFlowService], exports: [NextAuthGuard, ApiAuthGuard], diff --git a/apps/api/v2/src/modules/auth/strategies/api-auth/api-auth.strategy.e2e-spec.ts b/apps/api/v2/src/modules/auth/strategies/api-auth/api-auth.strategy.e2e-spec.ts index 11717d3bbc..bbc52b6dbf 100644 --- a/apps/api/v2/src/modules/auth/strategies/api-auth/api-auth.strategy.e2e-spec.ts +++ b/apps/api/v2/src/modules/auth/strategies/api-auth/api-auth.strategy.e2e-spec.ts @@ -4,6 +4,7 @@ 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"; +import { MembershipsModule } from "@/modules/memberships/memberships.module"; import { OAuthClientRepository } from "@/modules/oauth-clients/oauth-client.repository"; import { OAuthFlowService } from "@/modules/oauth-clients/services/oauth-flow.service"; import { PrismaReadService } from "@/modules/prisma/prisma-read.service"; @@ -20,6 +21,7 @@ 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 { MembershipRepositoryFixture } from "test/fixtures/repository/membership.repository.fixture"; import { OAuthClientRepositoryFixture } from "test/fixtures/repository/oauth-client.repository.fixture"; import { ProfileRepositoryFixture } from "test/fixtures/repository/profiles.repository.fixture"; import { TeamRepositoryFixture } from "test/fixtures/repository/team.repository.fixture"; @@ -45,6 +47,7 @@ describe("ApiAuthStrategy", () => { let apiKeysRepositoryFixture: ApiKeysRepositoryFixture; let oAuthClientRepositoryFixture: OAuthClientRepositoryFixture; let profilesRepositoryFixture: ProfileRepositoryFixture; + let membershipRepositoryFixture: MembershipRepositoryFixture; const validApiKeyEmail = `api-auth-api-key-user-${randomString()}@api.com`; const validAccessTokenEmail = `api-auth-access-token-user-${randomString()}@api.com`; @@ -65,6 +68,7 @@ describe("ApiAuthStrategy", () => { }), ProfilesModule, TokensModule, + MembershipsModule, ], providers: [ MockedRedisService, @@ -91,6 +95,7 @@ describe("ApiAuthStrategy", () => { teamRepositoryFixture = new TeamRepositoryFixture(module); oAuthClientRepositoryFixture = new OAuthClientRepositoryFixture(module); profilesRepositoryFixture = new ProfileRepositoryFixture(module); + membershipRepositoryFixture = new MembershipRepositoryFixture(module); organization = await teamRepositoryFixture.create({ name: `api-auth-organization-1-${randomString()}` }); organizationTwo = await teamRepositoryFixture.create({ name: `api-auth-organization-2-${randomString()}`, @@ -121,6 +126,20 @@ describe("ApiAuthStrategy", () => { organization: { connect: { id: organizationTwo.id } }, }); + await membershipRepositoryFixture.create({ + user: { connect: { id: validOAuthUser.id } }, + team: { connect: { id: organization.id } }, + role: "OWNER", + accepted: true, + }); + + await membershipRepositoryFixture.create({ + user: { connect: { id: validOAuthUser.id } }, + team: { connect: { id: organizationTwo.id } }, + role: "OWNER", + accepted: true, + }); + const data = { logo: "logo-url", name: "name", diff --git a/apps/api/v2/src/modules/auth/strategies/api-auth/api-auth.strategy.ts b/apps/api/v2/src/modules/auth/strategies/api-auth/api-auth.strategy.ts index b1688e69b9..7f36d811a3 100644 --- a/apps/api/v2/src/modules/auth/strategies/api-auth/api-auth.strategy.ts +++ b/apps/api/v2/src/modules/auth/strategies/api-auth/api-auth.strategy.ts @@ -4,9 +4,9 @@ import { isOriginAllowed } from "@/lib/is-origin-allowed/is-origin-allowed"; import { BaseStrategy } from "@/lib/passport/strategies/types"; import { ApiKeysRepository } from "@/modules/api-keys/api-keys-repository"; import { DeploymentsService } from "@/modules/deployments/deployments.service"; +import { MembershipsRepository } from "@/modules/memberships/memberships.repository"; 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 { TokensService } from "@/modules/tokens/tokens.service"; import { UsersService } from "@/modules/users/services/users.service"; @@ -45,8 +45,8 @@ export class ApiAuthStrategy extends PassportStrategy(BaseStrategy, "api-auth") private readonly userRepository: UsersRepository, private readonly apiKeyRepository: ApiKeysRepository, private readonly oauthRepository: OAuthClientRepository, - private readonly profilesRepository: ProfilesRepository, - private readonly usersService: UsersService + private readonly usersService: UsersService, + private readonly membershipsRepository: MembershipsRepository ) { super(); } @@ -172,7 +172,9 @@ export class ApiAuthStrategy extends PassportStrategy(BaseStrategy, "api-auth") throw new UnauthorizedException("ApiAuthStrategy - oAuth client - Invalid client secret"); } - const platformCreatorId = await this.profilesRepository.getPlatformOwnerUserId(client.organizationId); + const platformCreatorId = + (await this.membershipsRepository.findPlatformOwnerUserId(client.organizationId)) || + (await this.membershipsRepository.findPlatformAdminUserId(client.organizationId)); if (!platformCreatorId) { throw new UnauthorizedException( diff --git a/apps/api/v2/src/modules/memberships/memberships.repository.ts b/apps/api/v2/src/modules/memberships/memberships.repository.ts index e5d9d8cfdd..d51de8e7e8 100644 --- a/apps/api/v2/src/modules/memberships/memberships.repository.ts +++ b/apps/api/v2/src/modules/memberships/memberships.repository.ts @@ -20,6 +20,36 @@ export class MembershipsRepository { return membership; } + async findPlatformOwnerUserId(organizationId: number): Promise { + const owner = await this.dbRead.prisma.membership.findFirst({ + where: { + teamId: organizationId, + role: "OWNER", + accepted: true, + }, + select: { + userId: true, + }, + }); + + return owner?.userId ?? undefined; + } + + async findPlatformAdminUserId(organizationId: number): Promise { + const admin = await this.dbRead.prisma.membership.findFirst({ + where: { + teamId: organizationId, + role: "ADMIN", + accepted: true, + }, + select: { + userId: true, + }, + }); + + return admin?.userId ?? undefined; + } + async findMembershipByTeamId(teamId: number, userId: number) { const membership = await this.dbRead.prisma.membership.findUnique({ where: { diff --git a/apps/api/v2/src/modules/oauth-clients/controllers/oauth-flow/oauth-flow.controller.e2e-spec.ts b/apps/api/v2/src/modules/oauth-clients/controllers/oauth-flow/oauth-flow.controller.e2e-spec.ts index eb5bf0c419..65a6c81aa1 100644 --- a/apps/api/v2/src/modules/oauth-clients/controllers/oauth-flow/oauth-flow.controller.e2e-spec.ts +++ b/apps/api/v2/src/modules/oauth-clients/controllers/oauth-flow/oauth-flow.controller.e2e-spec.ts @@ -15,6 +15,7 @@ import { NestExpressApplication } from "@nestjs/platform-express"; import { Test, TestingModule } from "@nestjs/testing"; import { PlatformOAuthClient, Team, User } from "@prisma/client"; import * as request from "supertest"; +import { MembershipRepositoryFixture } from "test/fixtures/repository/membership.repository.fixture"; import { OAuthClientRepositoryFixture } from "test/fixtures/repository/oauth-client.repository.fixture"; import { OrganizationRepositoryFixture } from "test/fixtures/repository/organization.repository.fixture"; import { ProfileRepositoryFixture } from "test/fixtures/repository/profiles.repository.fixture"; @@ -63,6 +64,7 @@ describe("OAuthFlow Endpoints", () => { let organizationsRepositoryFixture: OrganizationRepositoryFixture; let oAuthClientsRepositoryFixture: OAuthClientRepositoryFixture; let profilesRepositoryFixture: ProfileRepositoryFixture; + let membershipRepositoryFixture: MembershipRepositoryFixture; let user: User; let organization: Team; @@ -89,6 +91,7 @@ describe("OAuthFlow Endpoints", () => { organizationsRepositoryFixture = new OrganizationRepositoryFixture(moduleRef); usersRepositoryFixtures = new UserRepositoryFixture(moduleRef); profilesRepositoryFixture = new ProfileRepositoryFixture(moduleRef); + membershipRepositoryFixture = new MembershipRepositoryFixture(moduleRef); user = await usersRepositoryFixtures.create({ email: userEmail, @@ -104,6 +107,13 @@ describe("OAuthFlow Endpoints", () => { movedFromUser: { connect: { id: user.id } }, organization: { connect: { id: organization.id } }, }); + + await membershipRepositoryFixture.create({ + user: { connect: { id: user.id } }, + team: { connect: { id: organization.id } }, + role: "OWNER", + accepted: true, + }); oAuthClient = await createOAuthClient(organization.id); });