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:
25 files changed
+233
-42
No files matched your search
@@ -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;
|
||||
}
|
||||
}
|
||||
+3
-2
@@ -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);
|
||||
|
||||
|
||||
+7
-6
@@ -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(
|
||||
|
||||
+3
-2
@@ -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);
|
||||
|
||||
+5
-4
@@ -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({
|
||||
|
||||
+2
-1
@@ -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) {}
|
||||
|
||||
+2
-1
@@ -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(
|
||||
|
||||
+5
-4
@@ -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);
|
||||
|
||||
|
||||
+4
-3
@@ -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();
|
||||
});
|
||||
});
|
||||
|
||||
+2
-1
@@ -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(
|
||||
|
||||
+13
-6
@@ -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,
|
||||
});
|
||||
|
||||
+2
-1
@@ -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) {}
|
||||
|
||||
+2
-1
@@ -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 {
|
||||
|
||||
+11
-7
@@ -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"),
|
||||
|
||||
+5
@@ -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;
|
||||
@@ -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 {
|
||||
|
||||
@@ -1067,6 +1067,7 @@ async function main() {
|
||||
organizationSettings: {
|
||||
isOrganizationVerified: true,
|
||||
orgAutoAcceptEmail: "acme.com",
|
||||
isAdminAPIEnabled: true,
|
||||
},
|
||||
},
|
||||
members: [
|
||||
|
||||
@@ -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,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
Reference in new issue
Block a user