From 1bf009f5f54a226f5a690c4fcfa0aa56a32f2b16 Mon Sep 17 00:00:00 2001 From: sean-brydon <55134778+sean-brydon@users.noreply.github.com> Date: Wed, 25 May 2022 16:21:18 +0100 Subject: [PATCH] Disable Impersonation Option (#2880) * Disable Impersonation * Update Description Copy Co-authored-by: Peer Richelsen --- .../security/DisableUserImpersonation.tsx | 55 +++++++++++ .../impersonation/ImpersonationProvider.ts | 4 + apps/web/pages/settings/security.tsx | 2 + apps/web/public/static/locales/en/common.json | 2 + apps/web/server/createContext.ts | 1 + apps/web/server/routers/viewer.tsx | 2 + .../migration.sql | 2 + packages/prisma/schema.prisma | 97 ++++++++++--------- 8 files changed, 117 insertions(+), 48 deletions(-) create mode 100644 apps/web/components/security/DisableUserImpersonation.tsx create mode 100644 packages/prisma/migrations/20220525110759_add_user_impersonation_toggle/migration.sql diff --git a/apps/web/components/security/DisableUserImpersonation.tsx b/apps/web/components/security/DisableUserImpersonation.tsx new file mode 100644 index 0000000000..66c978398d --- /dev/null +++ b/apps/web/components/security/DisableUserImpersonation.tsx @@ -0,0 +1,55 @@ +import { useLocale } from "@calcom/lib/hooks/useLocale"; +import showToast from "@calcom/lib/notification"; +import Button from "@calcom/ui/Button"; + +import { trpc } from "@lib/trpc"; + +import Badge from "@components/ui/Badge"; + +const DisableUserImpersonation = ({ disableImpersonation }: { disableImpersonation: boolean }) => { + const utils = trpc.useContext(); + + const { t } = useLocale(); + + const mutation = trpc.useMutation("viewer.updateProfile", { + onSuccess: async () => { + showToast(t("your_user_profile_updated_successfully"), "success"); + await utils.invalidateQueries(["viewer.me"]); + }, + async onSettled() { + await utils.invalidateQueries(["viewer.i18n"]); + }, + }); + + return ( + <> +
+
+
+

+ {t("user_impersonation_heading")} +

+ + {!disableImpersonation ? t("enabled") : t("disabled")} + +
+

{t("user_impersonation_description")}

+
+
+ +
+
+ + ); +}; + +export default DisableUserImpersonation; diff --git a/apps/web/ee/lib/impersonation/ImpersonationProvider.ts b/apps/web/ee/lib/impersonation/ImpersonationProvider.ts index 331d5ba3c3..6d53b683f1 100644 --- a/apps/web/ee/lib/impersonation/ImpersonationProvider.ts +++ b/apps/web/ee/lib/impersonation/ImpersonationProvider.ts @@ -32,6 +32,10 @@ const ImpersonationProvider = CredentialsProvider({ throw new Error("This user does not exist"); } + if (user.disableImpersonation) { + throw new Error("This user has disabled Impersonation."); + } + // Log impersonations for audit purposes await prisma.impersonations.create({ data: { diff --git a/apps/web/pages/settings/security.tsx b/apps/web/pages/settings/security.tsx index 58e8e57ea3..1fda179798 100644 --- a/apps/web/pages/settings/security.tsx +++ b/apps/web/pages/settings/security.tsx @@ -11,6 +11,7 @@ import { trpc } from "@lib/trpc"; import SettingsShell from "@components/SettingsShell"; import Shell from "@components/Shell"; import ChangePasswordSection from "@components/security/ChangePasswordSection"; +import DisableUserImpersonation from "@components/security/DisableUserImpersonation"; import TwoFactorAuthSection from "@components/security/TwoFactorAuthSection"; export default function Security() { @@ -39,6 +40,7 @@ export default function Security() { + )} diff --git a/apps/web/public/static/locales/en/common.json b/apps/web/public/static/locales/en/common.json index 56f74193ae..d436894040 100644 --- a/apps/web/public/static/locales/en/common.json +++ b/apps/web/public/static/locales/en/common.json @@ -816,6 +816,8 @@ "confirmation_page_gif": "Gif for confirmation page", "search": "Search", "impersonate": "Impersonate", + "user_impersonation_heading":"User Impersonation", + "user_impersonation_description":"Allows our support team to temporarily sign in as you to help us quickly resolve any issues you report to us.", "impersonate_user_tip": "All uses of this feature is audited.", "impersonating_user_warning": "Impersonating username \"{{user}}\".", "impersonating_stop_instructions": "<0>Click Here to stop.", diff --git a/apps/web/server/createContext.ts b/apps/web/server/createContext.ts index 6b4c6cfdc7..c57f1bbe97 100644 --- a/apps/web/server/createContext.ts +++ b/apps/web/server/createContext.ts @@ -44,6 +44,7 @@ async function getUserFromSession({ hideBranding: true, avatar: true, twoFactorEnabled: true, + disableImpersonation: true, identityProvider: true, brandColor: true, darkBrandColor: true, diff --git a/apps/web/server/routers/viewer.tsx b/apps/web/server/routers/viewer.tsx index c3cdf010ac..daaccbd8c4 100644 --- a/apps/web/server/routers/viewer.tsx +++ b/apps/web/server/routers/viewer.tsx @@ -86,6 +86,7 @@ const loggedInViewerRouter = createProtectedRouter() trialEndsAt: user.trialEndsAt, completedOnboarding: user.completedOnboarding, twoFactorEnabled: user.twoFactorEnabled, + disableImpersonation: user.disableImpersonation, identityProvider: user.identityProvider, brandColor: user.brandColor, darkBrandColor: user.darkBrandColor, @@ -681,6 +682,7 @@ const loggedInViewerRouter = createProtectedRouter() completedOnboarding: z.boolean().optional(), locale: z.string().optional(), timeFormat: z.number().optional(), + disableImpersonation: z.boolean().optional(), }), async resolve({ input, ctx }) { const { user, prisma } = ctx; diff --git a/packages/prisma/migrations/20220525110759_add_user_impersonation_toggle/migration.sql b/packages/prisma/migrations/20220525110759_add_user_impersonation_toggle/migration.sql new file mode 100644 index 0000000000..8872876bfa --- /dev/null +++ b/packages/prisma/migrations/20220525110759_add_user_impersonation_toggle/migration.sql @@ -0,0 +1,2 @@ +-- AlterTable +ALTER TABLE "users" ADD COLUMN "disableImpersonation" BOOLEAN NOT NULL DEFAULT false; diff --git a/packages/prisma/schema.prisma b/packages/prisma/schema.prisma index 8716d18912..e6023a097c 100644 --- a/packages/prisma/schema.prisma +++ b/packages/prisma/schema.prisma @@ -119,59 +119,60 @@ enum UserPermissionRole { } model User { - id Int @id @default(autoincrement()) - username String? @unique - name String? + id Int @id @default(autoincrement()) + username String? @unique + name String? /// @zod.email() - email String @unique - emailVerified DateTime? - password String? - bio String? - avatar String? - timeZone String @default("Europe/London") - weekStart String @default("Sunday") + email String @unique + emailVerified DateTime? + password String? + bio String? + avatar String? + timeZone String @default("Europe/London") + weekStart String @default("Sunday") // DEPRECATED - TO BE REMOVED - startTime Int @default(0) - endTime Int @default(1440) + startTime Int @default(0) + endTime Int @default(1440) // - bufferTime Int @default(0) - hideBranding Boolean @default(false) - theme String? - createdDate DateTime @default(now()) @map(name: "created") - trialEndsAt DateTime? - eventTypes EventType[] @relation("user_eventtype") - credentials Credential[] - teams Membership[] - bookings Booking[] - schedules Schedule[] - defaultScheduleId Int? - selectedCalendars SelectedCalendar[] - completedOnboarding Boolean @default(false) - locale String? - timeFormat Int? @default(12) - twoFactorSecret String? - twoFactorEnabled Boolean @default(false) - identityProvider IdentityProvider @default(CAL) - identityProviderId String? - availability Availability[] - invitedTo Int? - plan UserPlan @default(TRIAL) - webhooks Webhook[] - brandColor String @default("#292929") - darkBrandColor String @default("#fafafa") + bufferTime Int @default(0) + hideBranding Boolean @default(false) + theme String? + createdDate DateTime @default(now()) @map(name: "created") + trialEndsAt DateTime? + eventTypes EventType[] @relation("user_eventtype") + credentials Credential[] + teams Membership[] + bookings Booking[] + schedules Schedule[] + defaultScheduleId Int? + selectedCalendars SelectedCalendar[] + completedOnboarding Boolean @default(false) + locale String? + timeFormat Int? @default(12) + twoFactorSecret String? + twoFactorEnabled Boolean @default(false) + identityProvider IdentityProvider @default(CAL) + identityProviderId String? + availability Availability[] + invitedTo Int? + plan UserPlan @default(TRIAL) + webhooks Webhook[] + brandColor String @default("#292929") + darkBrandColor String @default("#fafafa") // the location where the events will end up - destinationCalendar DestinationCalendar? - away Boolean @default(false) + destinationCalendar DestinationCalendar? + away Boolean @default(false) // participate in dynamic group booking or not - allowDynamicBooking Boolean? @default(true) - metadata Json? - verified Boolean? @default(false) - role UserPermissionRole @default(USER) - impersonatedUsers Impersonations[] @relation("impersonated_user") - impersonatedBy Impersonations[] @relation("impersonated_by_user") - apiKeys ApiKey[] - accounts Account[] - sessions Session[] + allowDynamicBooking Boolean? @default(true) + metadata Json? + verified Boolean? @default(false) + role UserPermissionRole @default(USER) + disableImpersonation Boolean @default(false) + impersonatedUsers Impersonations[] @relation("impersonated_user") + impersonatedBy Impersonations[] @relation("impersonated_by_user") + apiKeys ApiKey[] + accounts Account[] + sessions Session[] Feedback Feedback[] @@map(name: "users")