Skip to content

Commit

Permalink
Move local admin migration out of app.ts
Browse files Browse the repository at this point in the history
  • Loading branch information
vitoreiji committed Oct 21, 2024
1 parent e20db0f commit 6d8c71b
Show file tree
Hide file tree
Showing 4 changed files with 73 additions and 55 deletions.
22 changes: 22 additions & 0 deletions src/common/api/common/utils/UserUtils.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
import { AccountType, GroupType } from "../TutanotaConstants.js"
import { User } from "../../entities/sys/TypeRefs.js"

/**
* Checks if the current user is an admin of the customer.
* @return True if the user is an admin
*/
export function isGlobalAdmin(user: User): boolean {
if (isInternalUser(user)) {
return user.memberships.some((m) => m.groupType === GroupType.Admin)
} else {
return false
}
}

/**
* Provides the information if an internal user is logged in.
* @return True if an internal user is logged in, false if no user or an external user is logged in.
*/
export function isInternalUser(user: User): boolean {
return user.accountType !== AccountType.EXTERNAL
}
9 changes: 3 additions & 6 deletions src/common/api/main/UserController.ts
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,7 @@ import { SessionType } from "../common/SessionType"
import { IServiceExecutor } from "../common/ServiceRequest.js"
import { isCustomizationEnabledForCustomer } from "../common/utils/CustomerUtils.js"
import { EntityUpdateData, isUpdateForTypeRef } from "../common/utils/EntityUpdateUtils.js"
import { isGlobalAdmin, isInternalUser } from "../common/utils/UserUtils.js"

assertMainOrNode()

Expand Down Expand Up @@ -86,11 +87,7 @@ export class UserController {
* @return True if the user is an admin
*/
isGlobalAdmin(): boolean {
if (this.isInternalUser()) {
return this.user.memberships.some((m) => m.groupType === GroupType.Admin)
} else {
return false
}
return isGlobalAdmin(this.user)
}

/**
Expand All @@ -110,7 +107,7 @@ export class UserController {
* @return True if an internal user is logged in, false if no user or an external user is logged in.
*/
isInternalUser(): boolean {
return this.user.accountType !== AccountType.EXTERNAL
return isInternalUser(this.user)
}

loadCustomer(): Promise<Customer> {
Expand Down
48 changes: 47 additions & 1 deletion src/common/api/worker/facades/lazy/GroupManagementFacade.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,10 +10,16 @@ import {
} from "../../../entities/tutanota/TypeRefs.js"
import { assertNotNull, freshVersioned, getFirstOrThrow, neverNull } from "@tutao/tutanota-utils"
import {
AdministratedGroup,
AdministratedGroupTypeRef,
createLocalAdminGroupReplacementData,
createLocalAdminRemovalPostIn,
createMembershipAddData,
createMembershipRemoveData,
CustomerTypeRef,
Group,
GroupInfo,
GroupInfoTypeRef,
GroupTypeRef,
LocalAdminGroupReplacementData,
User,
Expand All @@ -24,7 +30,7 @@ import { EntityClient } from "../../../common/EntityClient.js"
import { assertWorkerOrNode } from "../../../common/Env.js"
import { IServiceExecutor } from "../../../common/ServiceRequest.js"
import { CalendarService, ContactListGroupService, MailGroupService, TemplateGroupService } from "../../../entities/tutanota/Services.js"
import { MembershipService } from "../../../entities/sys/Services.js"
import { LocalAdminRemovalService, MembershipService } from "../../../entities/sys/Services.js"
import { UserFacade } from "../UserFacade.js"
import { ProgrammingError } from "../../../common/error/ProgrammingError.js"
import { PQFacade } from "../PQFacade.js"
Expand All @@ -33,6 +39,7 @@ import { CacheManagementFacade } from "./CacheManagementFacade.js"
import { CryptoWrapper, encryptKeyWithVersionedKey, encryptString, VersionedKey } from "../../crypto/CryptoWrapper.js"
import { AsymmetricCryptoFacade } from "../../crypto/AsymmetricCryptoFacade.js"
import { AesKey, PQKeyPairs } from "@tutao/tutanota-crypto"
import { isGlobalAdmin } from "../../../common/utils/UserUtils.js"

assertWorkerOrNode()

Expand Down Expand Up @@ -357,4 +364,43 @@ export class GroupManagementFacade {
})
return groupUpdate
}

/**
* Since local admins won't be supported anymore and will be removed we need to let the
* global admin access the locally administrated group data.
* As its name suggest this function migrate the users administrated by the local admins
* to the global admin of the customer so that the global admin can have direct
* encryption and decryption of its users group keys.
*/
async migrateLocalAdminsToGlobalAdmins() {
const user = this.userFacade.getLoggedInUser()
if (!isGlobalAdmin(user)) {
return
}

const customer = await this.entityClient.load(CustomerTypeRef, assertNotNull(user.customer))
const teamGroupInfos = await this.entityClient.loadAll(GroupInfoTypeRef, customer.teamGroups)
const localAdminGroupInfos = teamGroupInfos.filter((group) => group.groupType === GroupType.LocalAdmin)
const adminGroupId: Id = customer.adminGroup
const adminGroupKey = await this.keyLoaderFacade.getCurrentSymGroupKey(adminGroupId)
const postIn = createLocalAdminRemovalPostIn({ groupUpdates: [] })
const makeLocallyAdministratedGroupGloballyAdministrated = async (localAdminGroupInfo: GroupInfo) => {
const localAdminGroup = await this.entityClient.load(GroupTypeRef, localAdminGroupInfo.group)
const administratedGroupsListId = localAdminGroup.administratedGroups?.items
if (administratedGroupsListId == null) return null
const administratedGroups: Array<AdministratedGroup> = await this.entityClient.loadAll(AdministratedGroupTypeRef, administratedGroupsListId)

// we assume local admins never had their key roatation done and so their sym key version (requestedVersion) is stuck to 0 by default
const thisLocalAdminGroupKey = await this.keyLoaderFacade.loadSymGroupKey(localAdminGroup._id, 0)
administratedGroups.map(async (ag) => {
const thisRelatedGroupInfo = await this.entityClient.load(GroupInfoTypeRef, ag.groupInfo)
const thisRelatedGroup = await this.entityClient.load(GroupTypeRef, thisRelatedGroupInfo.group)

const groupUpdate = await this.replaceLocalAdminEncGroupKeyWithGlobalAdminEncGroupKey(adminGroupKey, thisLocalAdminGroupKey, thisRelatedGroup)
postIn.groupUpdates.push(groupUpdate)
})
}
localAdminGroupInfos.map(makeLocallyAdministratedGroupGloballyAdministrated)
await this.serviceExecutor.post(LocalAdminRemovalService, postIn)
}
}
49 changes: 1 addition & 48 deletions src/mail-app/app.ts
Original file line number Diff line number Diff line change
Expand Up @@ -39,16 +39,6 @@ import { disableErrorHandlingDuringLogout, handleUncaughtError } from "../common

import { AppType } from "../common/misc/ClientConstants.js"
import { ContactModel } from "../common/contactsFunctionality/ContactModel.js"
import {
AdministratedGroup,
AdministratedGroupTypeRef,
createLocalAdminRemovalPostIn,
GroupInfo,
GroupInfoTypeRef,
GroupTypeRef,
} from "../common/api/entities/sys/TypeRefs.js"
import { GroupType } from "../common/api/common/TutanotaConstants.js"
import { LocalAdminRemovalService } from "../common/api/entities/sys/Services.js"

assertMainOrNodeBoot()
bootFinished()
Expand Down Expand Up @@ -152,48 +142,11 @@ import("./translations/en.js")
async onFullLoginSuccess() {},
}
})
const migrateLocalAdminsToGlobalAdmins = async () => {
if (!mailLocator.logins.getUserController().isGlobalAdmin()) {
return
}

const customer = await mailLocator.logins.getUserController().loadCustomer()
const teamGroupInfos = await mailLocator.entityClient.loadAll(GroupInfoTypeRef, customer.teamGroups)
const localAdminGroupInfos = teamGroupInfos.filter((group) => group.groupType === GroupType.LocalAdmin)
const adminGroupId: Id = customer.adminGroup
const adminGroupKey = await mailLocator.keyLoaderFacade.getCurrentSymGroupKey(adminGroupId)
const postIn = createLocalAdminRemovalPostIn({ groupUpdates: [] })
const makeLocallyAdministratedGroupGloballyAdministrated = async (localAdminGroupInfo: GroupInfo) => {
const localAdminGroup = await mailLocator.entityClient.load(GroupTypeRef, localAdminGroupInfo.group)
const administratedGroupsListId = localAdminGroup.administratedGroups?.items
if (administratedGroupsListId == null) return null
const administratedGroups: Array<AdministratedGroup> = await mailLocator.entityClient.loadAll(
AdministratedGroupTypeRef,
administratedGroupsListId,
)
// we assume local admins never had their key roatation done and so their sym key version (requestedVersion) is stuck to 0 by default

const thisLocalAdminGroupKey = await mailLocator.keyLoaderFacade.loadSymGroupKey(localAdminGroup._id, 0)
administratedGroups.map(async (ag) => {
const thisRelatedGroupInfo = await mailLocator.entityClient.load(GroupInfoTypeRef, ag.groupInfo)
const thisRelatedGroup = await mailLocator.entityClient.load(GroupTypeRef, thisRelatedGroupInfo.group)

const groupUpdate = await mailLocator.groupManagementFacade.replaceLocalAdminEncGroupKeyWithGlobalAdminEncGroupKey(
adminGroupKey,
thisLocalAdminGroupKey,
thisRelatedGroup,
)
postIn.groupUpdates.push(groupUpdate)
})
}
localAdminGroupInfos.map(makeLocallyAdministratedGroupGloballyAdministrated)
await mailLocator.serviceExecutor.post(LocalAdminRemovalService, postIn)
}
mailLocator.logins.addPostLoginAction(async () => {
return {
async onPartialLoginSuccess() {},
async onFullLoginSuccess() {
return migrateLocalAdminsToGlobalAdmins()
return mailLocator.groupManagementFacade.migrateLocalAdminsToGlobalAdmins()
},
}
})
Expand Down

0 comments on commit 6d8c71b

Please sign in to comment.