feat: SysAdmin API Guard for Org Admins (#15867)

* adds column in schema

* adds org setting to enable/disable admin api

* adds guard to v1 v2

* add update query for isAdminAPIEnabled to be true for existing orgs

* adds contact sales

* fix typo

* fix tests

* fixup v2 guard

* v1 fix

* fix and add integration test

* fixes tests --WIP

* fix test --WIP

* fixupgit add src/modules

* WIP

* fix

* fix: e2e test org teams

* fix: e2e test org users

* review feedback

* requested change

* fix

---------

Co-authored-by: Keith Williams <keithwillcode@gmail.com>
Co-authored-by: Morgan <33722304+ThyMinimalDev@users.noreply.github.com>
Co-authored-by: Peer Richelsen <peeroke@gmail.com>
Co-authored-by: Morgan Vernay <morgan@cal.com>
This commit is contained in:
authored and GitHub committed 2024-08-02 14:35:54 +04:00
1 parent bb29edd30f
commit b91c767d0c
25 files changed
+233 -42

No files matched your search

+3
View File
@@ -19,6 +19,9 @@ export const isAdminGuard = async (req: NextApiRequest) => {
accepted: true,
team: {
isOrganization: true,
organizationSettings: {
isAdminAPIEnabled: true,
},
},
OR: [{ role: MembershipRole.OWNER }, { role: MembershipRole.ADMIN }],
},
@@ -58,7 +58,7 @@ describe("isAdmin guard", () => {
expect(scope).toBe(ScopeOfAdmin.SystemWide);
});
it("Returns org-wide admin when user is set as such", async () => {
it("Returns org-wide admin when user is set as such & admin API access is granted", async () => {
const { req } = createMocks<CustomNextApiRequest, CustomNextApiResponse>({
method: "POST",
body: {},
@@ -67,10 +67,21 @@ describe("isAdmin guard", () => {
const adminUser = await prisma.user.findFirstOrThrow({ where: { email: "owner1-acme@example.com" } });
req.userId = adminUser.id;
const { isAdmin, scope } = await isAdminGuard(req);
expect(isAdmin).toBe(true);
expect(scope).toBe(ScopeOfAdmin.OrgOwnerOrAdmin);
});
it("Returns no admin when user is set as org admin but admin API access is revoked", async () => {
const { req } = createMocks<CustomNextApiRequest, CustomNextApiResponse>({
method: "POST",
body: {},
});
const adminUser = await prisma.user.findFirstOrThrow({ where: { email: "owner1-dunder@example.com" } });
req.userId = adminUser.id;
const { isAdmin } = await isAdminGuard(req);
expect(isAdmin).toBe(false);
});
});
@@ -0,0 +1,62 @@
import { OrganizationsRepository } from "@/modules/organizations/organizations.repository";
import { RedisService } from "@/modules/redis/redis.service";
import { Injectable, CanActivate, ExecutionContext, ForbiddenException } from "@nestjs/common";
import { Request } from "express";
import { Team } from "@calcom/prisma/client";
type CachedData = {
org?: Team;
canAccess?: boolean;
};
@Injectable()
export class IsAdminAPIEnabledGuard implements CanActivate {
constructor(
private organizationsRepository: OrganizationsRepository,
private readonly redisService: RedisService
) {}
async canActivate(context: ExecutionContext): Promise<boolean> {
let canAccess = false;
const request = context.switchToHttp().getRequest<Request & { organization: Team }>();
const organizationId: string = request.params.orgId;
if (!organizationId) {
throw new ForbiddenException("No organization id found in request params.");
}
const REDIS_CACHE_KEY = `apiv2:org:${organizationId}:guard:isAdminAccess`;
const cachedData = await this.redisService.redis.get(REDIS_CACHE_KEY);
if (cachedData) {
const { org: cachedOrg, canAccess: cachedCanAccess } = JSON.parse(cachedData) as CachedData;
if (cachedOrg?.id === Number(organizationId) && cachedCanAccess !== undefined) {
request.organization = cachedOrg;
return cachedCanAccess;
}
}
const org = await this.organizationsRepository.findById(Number(organizationId));
if (org?.isOrganization && !org?.isPlatform) {
const adminAPIAccessIsEnabledInOrg = await this.organizationsRepository.fetchOrgAdminApiStatus(
Number(organizationId)
);
if (!adminAPIAccessIsEnabledInOrg) {
throw new ForbiddenException(
`Organization does not have Admin API access, please contact https://cal.com/sales to upgrade`
);
}
}
canAccess = true;
org &&
(await this.redisService.redis.set(
REDIS_CACHE_KEY,
JSON.stringify({ org: org, canAccess } satisfies CachedData),
"EX",
300
));
return canAccess;
}
}
@@ -15,6 +15,7 @@ import { Test, TestingModule } from "@nestjs/testing";
import { PlatformOAuthClient, Team, User } from "@prisma/client";
import * as request from "supertest";
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";
import { TeamRepositoryFixture } from "test/fixtures/repository/team.repository.fixture";
import { UserRepositoryFixture } from "test/fixtures/repository/users.repository.fixture";
@@ -57,7 +58,7 @@ describe("OAuthFlow Endpoints", () => {
let app: INestApplication;
let usersRepositoryFixtures: UserRepositoryFixture;
let organizationsRepositoryFixture: TeamRepositoryFixture;
let organizationsRepositoryFixture: OrganizationRepositoryFixture;
let oAuthClientsRepositoryFixture: OAuthClientRepositoryFixture;
let profilesRepositoryFixture: ProfileRepositoryFixture;
@@ -83,7 +84,7 @@ describe("OAuthFlow Endpoints", () => {
await app.init();
oAuthClientsRepositoryFixture = new OAuthClientRepositoryFixture(moduleRef);
organizationsRepositoryFixture = new TeamRepositoryFixture(moduleRef);
organizationsRepositoryFixture = new OrganizationRepositoryFixture(moduleRef);
usersRepositoryFixtures = new UserRepositoryFixture(moduleRef);
profilesRepositoryFixture = new ProfileRepositoryFixture(moduleRef);
@@ -4,6 +4,7 @@ import { GetUser } from "@/modules/auth/decorators/get-user/get-user.decorator";
import { Roles } from "@/modules/auth/decorators/roles/roles.decorator";
import { ApiAuthGuard } from "@/modules/auth/guards/api-auth/api-auth.guard";
import { PlatformPlanGuard } from "@/modules/auth/guards/billing/platform-plan.guard";
import { IsAdminAPIEnabledGuard } from "@/modules/auth/guards/organizations/is-admin-api-enabled.guard";
import { IsOrgGuard } from "@/modules/auth/guards/organizations/is-org.guard";
import { RolesGuard } from "@/modules/auth/guards/roles/roles.guard";
import { IsTeamInOrg } from "@/modules/auth/guards/teams/is-team-in-org.guard";
@@ -49,7 +50,7 @@ export class OrganizationsEventTypesController {
@Roles("TEAM_ADMIN")
@PlatformPlan("ESSENTIALS")
@UseGuards(ApiAuthGuard, IsOrgGuard, RolesGuard, IsTeamInOrg, PlatformPlanGuard)
@UseGuards(ApiAuthGuard, IsOrgGuard, RolesGuard, IsTeamInOrg, PlatformPlanGuard, IsAdminAPIEnabledGuard)
@Post("/teams/:teamId/event-types")
async createTeamEventType(
@GetUser() user: UserWithProfile,
@@ -72,7 +73,7 @@ export class OrganizationsEventTypesController {
@Roles("TEAM_ADMIN")
@PlatformPlan("ESSENTIALS")
@UseGuards(ApiAuthGuard, IsOrgGuard, RolesGuard, IsTeamInOrg, PlatformPlanGuard)
@UseGuards(ApiAuthGuard, IsOrgGuard, RolesGuard, IsTeamInOrg, PlatformPlanGuard, IsAdminAPIEnabledGuard)
@Get("/teams/:teamId/event-types/:eventTypeId")
async getTeamEventType(
@Param("teamId", ParseIntPipe) teamId: number,
@@ -90,7 +91,7 @@ export class OrganizationsEventTypesController {
};
}
@UseGuards(IsOrgGuard, IsTeamInOrg)
@UseGuards(IsOrgGuard, IsTeamInOrg, IsAdminAPIEnabledGuard)
@Get("/teams/:teamId/event-types")
async getTeamEventTypes(
@Param("teamId", ParseIntPipe) teamId: number,
@@ -116,7 +117,7 @@ export class OrganizationsEventTypesController {
@Roles("TEAM_ADMIN")
@PlatformPlan("ESSENTIALS")
@UseGuards(ApiAuthGuard, IsOrgGuard, RolesGuard, PlatformPlanGuard)
@UseGuards(ApiAuthGuard, IsOrgGuard, RolesGuard, PlatformPlanGuard, IsAdminAPIEnabledGuard)
@Get("/teams/event-types")
async getTeamsEventTypes(
@Param("orgId", ParseIntPipe) orgId: number,
@@ -133,7 +134,7 @@ export class OrganizationsEventTypesController {
@Roles("TEAM_ADMIN")
@PlatformPlan("ESSENTIALS")
@UseGuards(ApiAuthGuard, IsOrgGuard, RolesGuard, IsTeamInOrg, PlatformPlanGuard)
@UseGuards(ApiAuthGuard, IsOrgGuard, RolesGuard, IsTeamInOrg, PlatformPlanGuard, IsAdminAPIEnabledGuard)
@Patch("/teams/:teamId/event-types/:eventTypeId")
async updateTeamEventType(
@Param("teamId", ParseIntPipe) teamId: number,
@@ -156,7 +157,7 @@ export class OrganizationsEventTypesController {
@Roles("TEAM_ADMIN")
@PlatformPlan("ESSENTIALS")
@UseGuards(ApiAuthGuard, IsOrgGuard, RolesGuard, IsTeamInOrg, PlatformPlanGuard)
@UseGuards(ApiAuthGuard, IsOrgGuard, RolesGuard, IsTeamInOrg, PlatformPlanGuard, IsAdminAPIEnabledGuard)
@Delete("/teams/:teamId/event-types/:eventTypeId")
@HttpCode(HttpStatus.OK)
async deleteTeamEventType(
@@ -10,6 +10,7 @@ import { User } from "@prisma/client";
import * as request from "supertest";
import { EventTypesRepositoryFixture } from "test/fixtures/repository/event-types.repository.fixture";
import { MembershipRepositoryFixture } from "test/fixtures/repository/membership.repository.fixture";
import { OrganizationRepositoryFixture } from "test/fixtures/repository/organization.repository.fixture";
import { ProfileRepositoryFixture } from "test/fixtures/repository/profiles.repository.fixture";
import { TeamRepositoryFixture } from "test/fixtures/repository/team.repository.fixture";
import { UserRepositoryFixture } from "test/fixtures/repository/users.repository.fixture";
@@ -30,7 +31,7 @@ describe("Organizations Event Types Endpoints", () => {
let app: INestApplication;
let userRepositoryFixture: UserRepositoryFixture;
let organizationsRepositoryFixture: TeamRepositoryFixture;
let organizationsRepositoryFixture: OrganizationRepositoryFixture;
let teamsRepositoryFixture: TeamRepositoryFixture;
let membershipsRepositoryFixture: MembershipRepositoryFixture;
let profileRepositoryFixture: ProfileRepositoryFixture;
@@ -63,7 +64,7 @@ describe("Organizations Event Types Endpoints", () => {
).compile();
userRepositoryFixture = new UserRepositoryFixture(moduleRef);
organizationsRepositoryFixture = new TeamRepositoryFixture(moduleRef);
organizationsRepositoryFixture = new OrganizationRepositoryFixture(moduleRef);
teamsRepositoryFixture = new TeamRepositoryFixture(moduleRef);
membershipsRepositoryFixture = new MembershipRepositoryFixture(moduleRef);
profileRepositoryFixture = new ProfileRepositoryFixture(moduleRef);
@@ -11,6 +11,7 @@ import { Test } from "@nestjs/testing";
import { User } from "@prisma/client";
import * as request from "supertest";
import { MembershipRepositoryFixture } from "test/fixtures/repository/membership.repository.fixture";
import { OrganizationRepositoryFixture } from "test/fixtures/repository/organization.repository.fixture";
import { TeamRepositoryFixture } from "test/fixtures/repository/team.repository.fixture";
import { UserRepositoryFixture } from "test/fixtures/repository/users.repository.fixture";
import { withApiAuth } from "test/utils/withApiAuth";
@@ -24,7 +25,7 @@ describe("Organizations Memberships Endpoints", () => {
let app: INestApplication;
let userRepositoryFixture: UserRepositoryFixture;
let organizationsRepositoryFixture: TeamRepositoryFixture;
let organizationsRepositoryFixture: OrganizationRepositoryFixture;
let membershipsRepositoryFixture: MembershipRepositoryFixture;
let org: Team;
@@ -51,7 +52,7 @@ describe("Organizations Memberships Endpoints", () => {
).compile();
userRepositoryFixture = new UserRepositoryFixture(moduleRef);
organizationsRepositoryFixture = new TeamRepositoryFixture(moduleRef);
organizationsRepositoryFixture = new OrganizationRepositoryFixture(moduleRef);
membershipsRepositoryFixture = new MembershipRepositoryFixture(moduleRef);
user = await userRepositoryFixture.create({
@@ -210,7 +211,7 @@ describe("Organizations Memberships Endpoints", () => {
let app: INestApplication;
let userRepositoryFixture: UserRepositoryFixture;
let organizationsRepositoryFixture: TeamRepositoryFixture;
let organizationsRepositoryFixture: OrganizationRepositoryFixture;
let membershipsRepositoryFixture: MembershipRepositoryFixture;
let org: Team;
@@ -228,7 +229,7 @@ describe("Organizations Memberships Endpoints", () => {
).compile();
userRepositoryFixture = new UserRepositoryFixture(moduleRef);
organizationsRepositoryFixture = new TeamRepositoryFixture(moduleRef);
organizationsRepositoryFixture = new OrganizationRepositoryFixture(moduleRef);
membershipsRepositoryFixture = new MembershipRepositoryFixture(moduleRef);
user = await userRepositoryFixture.create({
@@ -5,6 +5,7 @@ import { Roles } from "@/modules/auth/decorators/roles/roles.decorator";
import { ApiAuthGuard } from "@/modules/auth/guards/api-auth/api-auth.guard";
import { PlatformPlanGuard } from "@/modules/auth/guards/billing/platform-plan.guard";
import { IsMembershipInOrg } from "@/modules/auth/guards/memberships/is-membership-in-org.guard";
import { IsAdminAPIEnabledGuard } from "@/modules/auth/guards/organizations/is-admin-api-enabled.guard";
import { IsOrgGuard } from "@/modules/auth/guards/organizations/is-org.guard";
import { RolesGuard } from "@/modules/auth/guards/roles/roles.guard";
import { CreateOrgMembershipDto } from "@/modules/organizations/inputs/create-organization-membership.input";
@@ -41,7 +42,7 @@ import { Membership } from "@calcom/prisma/client";
path: "/v2/organizations/:orgId/memberships",
version: API_VERSIONS_VALUES,
})
@UseGuards(ApiAuthGuard, IsOrgGuard, RolesGuard, PlatformPlanGuard)
@UseGuards(ApiAuthGuard, IsOrgGuard, RolesGuard, PlatformPlanGuard, IsAdminAPIEnabledGuard)
@DocsTags("Organizations Memberships")
export class OrganizationsMembershipsController {
constructor(private organizationsMembershipService: OrganizationsMembershipService) {}
@@ -4,6 +4,7 @@ import { PlatformPlan } from "@/modules/auth/decorators/billing/platform-plan.de
import { Roles } from "@/modules/auth/decorators/roles/roles.decorator";
import { ApiAuthGuard } from "@/modules/auth/guards/api-auth/api-auth.guard";
import { PlatformPlanGuard } from "@/modules/auth/guards/billing/platform-plan.guard";
import { IsAdminAPIEnabledGuard } from "@/modules/auth/guards/organizations/is-admin-api-enabled.guard";
import { IsOrgGuard } from "@/modules/auth/guards/organizations/is-org.guard";
import { RolesGuard } from "@/modules/auth/guards/roles/roles.guard";
import { IsUserInOrg } from "@/modules/auth/guards/users/is-user-in-org.guard";
@@ -40,7 +41,7 @@ import { SkipTakePagination } from "@calcom/platform-types";
path: "/v2/organizations/:orgId",
version: API_VERSIONS_VALUES,
})
@UseGuards(ApiAuthGuard, IsOrgGuard, RolesGuard, PlatformPlanGuard)
@UseGuards(ApiAuthGuard, IsOrgGuard, RolesGuard, PlatformPlanGuard, IsAdminAPIEnabledGuard)
@DocsTags("Organizations Schedules")
export class OrganizationsSchedulesController {
constructor(
@@ -8,6 +8,7 @@ import { NestExpressApplication } from "@nestjs/platform-express";
import { Test } from "@nestjs/testing";
import * as request from "supertest";
import { MembershipRepositoryFixture } from "test/fixtures/repository/membership.repository.fixture";
import { OrganizationRepositoryFixture } from "test/fixtures/repository/organization.repository.fixture";
import { ProfileRepositoryFixture } from "test/fixtures/repository/profiles.repository.fixture";
import { TeamRepositoryFixture } from "test/fixtures/repository/team.repository.fixture";
import { UserRepositoryFixture } from "test/fixtures/repository/users.repository.fixture";
@@ -31,7 +32,7 @@ describe("Organizations Schedules Endpoints", () => {
let app: INestApplication;
let userRepositoryFixture: UserRepositoryFixture;
let organizationsRepositoryFixture: TeamRepositoryFixture;
let organizationsRepositoryFixture: OrganizationRepositoryFixture;
let membershipFixtures: MembershipRepositoryFixture;
const userEmail = "mr-robot@schedules-api.com";
@@ -48,7 +49,7 @@ describe("Organizations Schedules Endpoints", () => {
).compile();
userRepositoryFixture = new UserRepositoryFixture(moduleRef);
organizationsRepositoryFixture = new TeamRepositoryFixture(moduleRef);
organizationsRepositoryFixture = new OrganizationRepositoryFixture(moduleRef);
membershipFixtures = new MembershipRepositoryFixture(moduleRef);
org = await organizationsRepositoryFixture.create({
@@ -110,7 +111,7 @@ describe("Organizations Schedules Endpoints", () => {
let app: INestApplication;
let userRepositoryFixture: UserRepositoryFixture;
let organizationsRepositoryFixture: TeamRepositoryFixture;
let organizationsRepositoryFixture: OrganizationRepositoryFixture;
let membershipFixtures: MembershipRepositoryFixture;
let profileRepositoryFixture: ProfileRepositoryFixture;
@@ -146,7 +147,7 @@ describe("Organizations Schedules Endpoints", () => {
).compile();
userRepositoryFixture = new UserRepositoryFixture(moduleRef);
organizationsRepositoryFixture = new TeamRepositoryFixture(moduleRef);
organizationsRepositoryFixture = new OrganizationRepositoryFixture(moduleRef);
membershipFixtures = new MembershipRepositoryFixture(moduleRef);
profileRepositoryFixture = new ProfileRepositoryFixture(moduleRef);
@@ -11,6 +11,7 @@ import { Test } from "@nestjs/testing";
import { User } from "@prisma/client";
import * as request from "supertest";
import { MembershipRepositoryFixture } from "test/fixtures/repository/membership.repository.fixture";
import { OrganizationRepositoryFixture } from "test/fixtures/repository/organization.repository.fixture";
import { ProfileRepositoryFixture } from "test/fixtures/repository/profiles.repository.fixture";
import { TeamRepositoryFixture } from "test/fixtures/repository/team.repository.fixture";
import { UserRepositoryFixture } from "test/fixtures/repository/users.repository.fixture";
@@ -25,7 +26,7 @@ describe("Organizations Teams Memberships Endpoints", () => {
let app: INestApplication;
let userRepositoryFixture: UserRepositoryFixture;
let organizationsRepositoryFixture: TeamRepositoryFixture;
let organizationsRepositoryFixture: OrganizationRepositoryFixture;
let teamsRepositoryFixture: TeamRepositoryFixture;
let profileRepositoryFixture: ProfileRepositoryFixture;
@@ -59,7 +60,7 @@ describe("Organizations Teams Memberships Endpoints", () => {
).compile();
userRepositoryFixture = new UserRepositoryFixture(moduleRef);
organizationsRepositoryFixture = new TeamRepositoryFixture(moduleRef);
organizationsRepositoryFixture = new OrganizationRepositoryFixture(moduleRef);
teamsRepositoryFixture = new TeamRepositoryFixture(moduleRef);
profileRepositoryFixture = new ProfileRepositoryFixture(moduleRef);
@@ -335,7 +336,7 @@ describe("Organizations Teams Memberships Endpoints", () => {
await userRepositoryFixture.deleteByEmail(nonOrgUser.email);
await userRepositoryFixture.deleteByEmail(user2.email);
await organizationsRepositoryFixture.delete(org.id);
await organizationsRepositoryFixture.delete(nonOrgTeam.id);
await teamsRepositoryFixture.delete(nonOrgTeam.id);
await app.close();
});
});
@@ -3,6 +3,7 @@ import { PlatformPlan } from "@/modules/auth/decorators/billing/platform-plan.de
import { Roles } from "@/modules/auth/decorators/roles/roles.decorator";
import { ApiAuthGuard } from "@/modules/auth/guards/api-auth/api-auth.guard";
import { PlatformPlanGuard } from "@/modules/auth/guards/billing/platform-plan.guard";
import { IsAdminAPIEnabledGuard } from "@/modules/auth/guards/organizations/is-admin-api-enabled.guard";
import { IsOrgGuard } from "@/modules/auth/guards/organizations/is-org.guard";
import { RolesGuard } from "@/modules/auth/guards/roles/roles.guard";
import { IsTeamInOrg } from "@/modules/auth/guards/teams/is-team-in-org.guard";
@@ -40,7 +41,7 @@ import { SkipTakePagination } from "@calcom/platform-types";
path: "/v2/organizations/:orgId/teams/:teamId/memberships",
version: API_VERSIONS_VALUES,
})
@UseGuards(ApiAuthGuard, IsOrgGuard, RolesGuard, IsTeamInOrg, PlatformPlanGuard)
@UseGuards(ApiAuthGuard, IsOrgGuard, RolesGuard, IsTeamInOrg, PlatformPlanGuard, IsAdminAPIEnabledGuard)
@DocsTags("Organizations Teams")
export class OrganizationsTeamsMembershipsController {
constructor(
@@ -12,6 +12,7 @@ import { 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 { TeamRepositoryFixture } from "test/fixtures/repository/team.repository.fixture";
import { UserRepositoryFixture } from "test/fixtures/repository/users.repository.fixture";
import { withApiAuth } from "test/utils/withApiAuth";
@@ -25,6 +26,7 @@ describe("Organizations Team Endpoints", () => {
let app: INestApplication;
let userRepositoryFixture: UserRepositoryFixture;
let organizationsRepositoryFixture: OrganizationRepositoryFixture;
let teamsRepositoryFixture: TeamRepositoryFixture;
let membershipsRepositoryFixture: MembershipRepositoryFixture;
@@ -46,6 +48,7 @@ describe("Organizations Team Endpoints", () => {
).compile();
userRepositoryFixture = new UserRepositoryFixture(moduleRef);
organizationsRepositoryFixture = new OrganizationRepositoryFixture(moduleRef);
teamsRepositoryFixture = new TeamRepositoryFixture(moduleRef);
membershipsRepositoryFixture = new MembershipRepositoryFixture(moduleRef);
@@ -54,7 +57,7 @@ describe("Organizations Team Endpoints", () => {
username: userEmail,
});
org = await teamsRepositoryFixture.create({
org = await organizationsRepositoryFixture.create({
name: "Test Organization",
isOrganization: true,
});
@@ -239,6 +242,7 @@ describe("Organizations Team Endpoints", () => {
let app: INestApplication;
let userRepositoryFixture: UserRepositoryFixture;
let organizationsRepositoryFixture: OrganizationRepositoryFixture;
let teamsRepositoryFixture: TeamRepositoryFixture;
let membershipsRepositoryFixture: MembershipRepositoryFixture;
@@ -258,6 +262,7 @@ describe("Organizations Team Endpoints", () => {
).compile();
userRepositoryFixture = new UserRepositoryFixture(moduleRef);
organizationsRepositoryFixture = new OrganizationRepositoryFixture(moduleRef);
teamsRepositoryFixture = new TeamRepositoryFixture(moduleRef);
membershipsRepositoryFixture = new MembershipRepositoryFixture(moduleRef);
@@ -266,7 +271,7 @@ describe("Organizations Team Endpoints", () => {
username: userEmail,
});
org = await teamsRepositoryFixture.create({
org = await organizationsRepositoryFixture.create({
name: "Test Organization",
isOrganization: true,
});
@@ -351,7 +356,7 @@ describe("Organizations Team Endpoints", () => {
let app: INestApplication;
let userRepositoryFixture: UserRepositoryFixture;
let organizationsRepositoryFixture: TeamRepositoryFixture;
let organizationsRepositoryFixture: OrganizationRepositoryFixture;
let teamsRepositoryFixture: TeamRepositoryFixture;
let membershipsRepositoryFixture: MembershipRepositoryFixture;
@@ -371,7 +376,7 @@ describe("Organizations Team Endpoints", () => {
).compile();
userRepositoryFixture = new UserRepositoryFixture(moduleRef);
organizationsRepositoryFixture = new TeamRepositoryFixture(moduleRef);
organizationsRepositoryFixture = new OrganizationRepositoryFixture(moduleRef);
teamsRepositoryFixture = new TeamRepositoryFixture(moduleRef);
membershipsRepositoryFixture = new MembershipRepositoryFixture(moduleRef);
@@ -478,6 +483,8 @@ describe("Organizations Team Endpoints", () => {
let userRepositoryFixture: UserRepositoryFixture;
let teamsRepositoryFixture: TeamRepositoryFixture;
let orgRepositoryFixture: OrganizationRepositoryFixture;
let membershipsRepositoryFixture: MembershipRepositoryFixture;
let oauthClientRepositoryFixture: OAuthClientRepositoryFixture;
@@ -502,13 +509,13 @@ describe("Organizations Team Endpoints", () => {
teamsRepositoryFixture = new TeamRepositoryFixture(moduleRef);
membershipsRepositoryFixture = new MembershipRepositoryFixture(moduleRef);
oauthClientRepositoryFixture = new OAuthClientRepositoryFixture(moduleRef);
orgRepositoryFixture = new OrganizationRepositoryFixture(moduleRef);
user = await userRepositoryFixture.create({
email: userEmail,
username: userEmail,
});
org = await teamsRepositoryFixture.create({
org = await orgRepositoryFixture.create({
name: "Platform Test Organization",
isOrganization: true,
});
@@ -5,6 +5,7 @@ import { GetUser } from "@/modules/auth/decorators/get-user/get-user.decorator";
import { Roles } from "@/modules/auth/decorators/roles/roles.decorator";
import { ApiAuthGuard } from "@/modules/auth/guards/api-auth/api-auth.guard";
import { PlatformPlanGuard } from "@/modules/auth/guards/billing/platform-plan.guard";
import { IsAdminAPIEnabledGuard } from "@/modules/auth/guards/organizations/is-admin-api-enabled.guard";
import { IsOrgGuard } from "@/modules/auth/guards/organizations/is-org.guard";
import { RolesGuard } from "@/modules/auth/guards/roles/roles.guard";
import { IsTeamInOrg } from "@/modules/auth/guards/teams/is-team-in-org.guard";
@@ -43,7 +44,7 @@ import { Team } from "@calcom/prisma/client";
path: "/v2/organizations/:orgId/teams",
version: API_VERSIONS_VALUES,
})
@UseGuards(ApiAuthGuard, IsOrgGuard, RolesGuard, PlatformPlanGuard)
@UseGuards(ApiAuthGuard, IsOrgGuard, RolesGuard, PlatformPlanGuard, IsAdminAPIEnabledGuard)
@DocsTags("Organizations Teams")
export class OrganizationsTeamsController {
constructor(private organizationsTeamsService: OrganizationsTeamsService) {}
@@ -5,6 +5,7 @@ import { GetUser } from "@/modules/auth/decorators/get-user/get-user.decorator";
import { Roles } from "@/modules/auth/decorators/roles/roles.decorator";
import { ApiAuthGuard } from "@/modules/auth/guards/api-auth/api-auth.guard";
import { PlatformPlanGuard } from "@/modules/auth/guards/billing/platform-plan.guard";
import { IsAdminAPIEnabledGuard } from "@/modules/auth/guards/organizations/is-admin-api-enabled.guard";
import { IsOrgGuard } from "@/modules/auth/guards/organizations/is-org.guard";
import { RolesGuard } from "@/modules/auth/guards/roles/roles.guard";
import { IsUserInOrg } from "@/modules/auth/guards/users/is-user-in-org.guard";
@@ -41,7 +42,7 @@ import { Team } from "@calcom/prisma/client";
version: API_VERSIONS_VALUES,
})
@UseInterceptors(ClassSerializerInterceptor)
@UseGuards(ApiAuthGuard, IsOrgGuard, RolesGuard, PlatformPlanGuard)
@UseGuards(ApiAuthGuard, IsOrgGuard, RolesGuard, PlatformPlanGuard, IsAdminAPIEnabledGuard)
@UseGuards(IsOrgGuard)
@DocsTags("Organizations Users")
export class OrganizationsUsersController {
@@ -10,6 +10,7 @@ import { Test } from "@nestjs/testing";
import * as request from "supertest";
import { EventTypesRepositoryFixture } from "test/fixtures/repository/event-types.repository.fixture";
import { MembershipRepositoryFixture } from "test/fixtures/repository/membership.repository.fixture";
import { OrganizationRepositoryFixture } from "test/fixtures/repository/organization.repository.fixture";
import { ProfileRepositoryFixture } from "test/fixtures/repository/profiles.repository.fixture";
import { TeamRepositoryFixture } from "test/fixtures/repository/team.repository.fixture";
import { UserRepositoryFixture } from "test/fixtures/repository/users.repository.fixture";
@@ -23,7 +24,7 @@ describe("Organizations Users Endpoints", () => {
let app: INestApplication;
let userRepositoryFixture: UserRepositoryFixture;
let organizationsRepositoryFixture: TeamRepositoryFixture;
let organizationsRepositoryFixture: OrganizationRepositoryFixture;
let membershipFixtures: MembershipRepositoryFixture;
let profileRepositoryFixture: ProfileRepositoryFixture;
@@ -40,7 +41,7 @@ describe("Organizations Users Endpoints", () => {
).compile();
userRepositoryFixture = new UserRepositoryFixture(moduleRef);
organizationsRepositoryFixture = new TeamRepositoryFixture(moduleRef);
organizationsRepositoryFixture = new OrganizationRepositoryFixture(moduleRef);
membershipFixtures = new MembershipRepositoryFixture(moduleRef);
profileRepositoryFixture = new ProfileRepositoryFixture(moduleRef);
@@ -113,7 +114,7 @@ describe("Organizations Users Endpoints", () => {
let app: INestApplication;
let profileRepositoryFixture: ProfileRepositoryFixture;
let userRepositoryFixture: UserRepositoryFixture;
let organizationsRepositoryFixture: TeamRepositoryFixture;
let organizationsRepositoryFixture: OrganizationRepositoryFixture;
let membershipFixtures: MembershipRepositoryFixture;
const userEmail = "admin1@org.com";
@@ -148,7 +149,7 @@ describe("Organizations Users Endpoints", () => {
userRepositoryFixture = new UserRepositoryFixture(moduleRef);
profileRepositoryFixture = new ProfileRepositoryFixture(moduleRef);
organizationsRepositoryFixture = new TeamRepositoryFixture(moduleRef);
organizationsRepositoryFixture = new OrganizationRepositoryFixture(moduleRef);
membershipFixtures = new MembershipRepositoryFixture(moduleRef);
org = await organizationsRepositoryFixture.create({
@@ -349,7 +350,8 @@ describe("Organizations Users Endpoints", () => {
let app: INestApplication;
let userRepositoryFixture: UserRepositoryFixture;
let organizationsRepositoryFixture: TeamRepositoryFixture;
let teamsRepositoryFixture: TeamRepositoryFixture;
let organizationsRepositoryFixture: OrganizationRepositoryFixture;
let eventTypesRepositoryFixture: EventTypesRepositoryFixture;
let membershipFixtures: MembershipRepositoryFixture;
let profileRepositoryFixture: ProfileRepositoryFixture;
@@ -370,7 +372,9 @@ describe("Organizations Users Endpoints", () => {
).compile();
userRepositoryFixture = new UserRepositoryFixture(moduleRef);
organizationsRepositoryFixture = new TeamRepositoryFixture(moduleRef);
teamsRepositoryFixture = new TeamRepositoryFixture(moduleRef);
organizationsRepositoryFixture = new OrganizationRepositoryFixture(moduleRef);
eventTypesRepositoryFixture = new EventTypesRepositoryFixture(moduleRef);
membershipFixtures = new MembershipRepositoryFixture(moduleRef);
profileRepositoryFixture = new ProfileRepositoryFixture(moduleRef);
@@ -380,7 +384,7 @@ describe("Organizations Users Endpoints", () => {
isOrganization: true,
});
team = await organizationsRepositoryFixture.create({
team = await teamsRepositoryFixture.create({
name: "Test org 4 team",
isOrganization: false,
parent: { connect: { id: org.id } },
@@ -102,4 +102,15 @@ export class OrganizationsRepository {
},
});
}
async fetchOrgAdminApiStatus(organizationId: number) {
return this.dbRead.prisma.organizationSettings.findUnique({
where: {
organizationId,
},
select: {
isAdminAPIEnabled: true,
},
});
}
}
@@ -0,0 +1,44 @@
import { PrismaReadService } from "@/modules/prisma/prisma-read.service";
import { PrismaWriteService } from "@/modules/prisma/prisma-write.service";
import { TestingModule } from "@nestjs/testing";
import { Prisma, Team } from "@prisma/client";
export class OrganizationRepositoryFixture {
private primaReadClient: PrismaReadService["prisma"];
private prismaWriteClient: PrismaWriteService["prisma"];
constructor(private readonly module: TestingModule) {
this.primaReadClient = module.get(PrismaReadService).prisma;
this.prismaWriteClient = module.get(PrismaWriteService).prisma;
}
async get(teamId: Team["id"]) {
return this.primaReadClient.team.findFirst({ where: { id: teamId } });
}
async create(data: Prisma.TeamCreateInput) {
return await this.prismaWriteClient.$transaction(async (prisma) => {
const team = await prisma.team.create({
data,
});
await prisma.organizationSettings.create({
data: {
organizationId: team.id,
isAdminAPIEnabled: true,
orgAutoAcceptEmail: "cal.com",
},
});
return team;
});
}
async delete(teamId: Team["id"]) {
return await this.prismaWriteClient.$transaction(async (prisma) => {
await prisma.organizationSettings.delete({
where: { organizationId: teamId },
});
return prisma.team.delete({ where: { id: teamId } });
});
}
}
@@ -2507,6 +2507,8 @@
"event_expired": "This event is expired",
"skip_contact_creation": "Skip creating contacts if they do not exist in {{appName}} ",
"skip_writing_to_calendar_note": "If your ICS link is read-only (e.g., Proton Calendar), check the box above to avoid errors. You'll also need to manually update your calendar for changes.",
"grant_admin_api": "Grant Admin API Access",
"revoke_admin_api": "Revoke Admin API Access",
"apple_connect_atom_label": "Connect Apple Calendar",
"apple_connect_atom_already_connected_label": "Connected Apple Calendar",
"apple_connect_atom_loading_label": "Checking Apple Calendar",
@@ -81,6 +81,7 @@ function AdminOrgTable() {
<ColumnTitle widthClassNames="w-auto">{t("reviewed")}</ColumnTitle>
<ColumnTitle widthClassNames="w-auto">{t("dns_configured")}</ColumnTitle>
<ColumnTitle widthClassNames="w-auto">{t("published")}</ColumnTitle>
<ColumnTitle widthClassNames="w-auto">{t("admin_api")}</ColumnTitle>
<ColumnTitle widthClassNames="w-auto">
<span className="sr-only">{t("edit")}</span>
</ColumnTitle>
@@ -129,6 +130,15 @@ function AdminOrgTable() {
)}
</div>
</Cell>
<Cell>
<div className="space-x-2">
{!org.organizationSettings?.isAdminAPIEnabled ? (
<Badge variant="red">{t("disabled")}</Badge>
) : (
<Badge variant="green">{t("enabled")}</Badge>
)}
</div>
</Cell>
<Cell widthClassNames="w-auto">
<div className="flex w-full justify-end">
<DropdownActions
@@ -185,6 +195,21 @@ function AdminOrgTable() {
},
]
: []),
{
id: "api",
label: org.organizationSettings?.isAdminAPIEnabled
? t("revoke_admin_api")
: t("grant_admin_api"),
onClick: () => {
updateMutation.mutate({
id: org.id,
organizationSettings: {
isAdminAPIEnabled: !org.organizationSettings?.isAdminAPIEnabled,
},
});
},
icon: "terminal" as const,
},
{
id: "delete",
label: t("delete"),
@@ -0,0 +1,5 @@
-- AlterTable
ALTER TABLE "OrganizationSettings" ADD COLUMN "isAdminAPIEnabled" BOOLEAN NOT NULL DEFAULT false;
-- User written Query: Update OrganizationSettings table to mark all existing organizations to have AdminAPI Enabled to not lose functionality.
UPDATE "OrganizationSettings" SET "isAdminAPIEnabled" = true;
+1
View File
@@ -431,6 +431,7 @@ model OrganizationSettings {
// It is used to allow super sensitive operations like 'impersonation of Org members by Org admin'
isAdminReviewed Boolean @default(false)
dSyncData DSyncData?
isAdminAPIEnabled Boolean @default(false)
}
enum MembershipRole {
+1
View File
@@ -1067,6 +1067,7 @@ async function main() {
organizationSettings: {
isOrganizationVerified: true,
orgAutoAcceptEmail: "acme.com",
isAdminAPIEnabled: true,
},
},
members: [
+1
View File
@@ -359,6 +359,7 @@ export const orgSettingsSchema = z
isOrganizationConfigured: z.boolean().optional(),
isAdminReviewed: z.boolean().optional(),
orgAutoAcceptEmail: z.string().optional(),
isAdminAPIEnabled: z.boolean().optional(),
})
.nullable();
export type userMetadataType = z.infer<typeof userMetadata>;
@@ -74,6 +74,9 @@ export const adminUpdateHandler = async ({ input }: AdminUpdateOptions) => {
isAdminReviewed: organizationSettings?.isAdminReviewed,
orgAutoAcceptEmail:
organizationSettings?.orgAutoAcceptEmail || existingOrg.organizationSettings?.orgAutoAcceptEmail,
isAdminAPIEnabled:
!!organizationSettings?.isAdminAPIEnabled ??
!!existingOrg.organizationSettings?.isAdminAPIEnabled,
},
});
}