Skip to content

Commit f6b1b33

Browse files
authored
UBERF-12509: Trusted accounts migration tool (#9652)
1 parent 63a6efd commit f6b1b33

File tree

5 files changed

+375
-3
lines changed

5 files changed

+375
-3
lines changed

dev/tool/package.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@
2121
"docker:staging": "../../common/scripts/docker_tag.sh hardcoreeng/tool staging",
2222
"docker:push": "../../common/scripts/docker_tag.sh hardcoreeng/tool",
2323
"run-local-mongo": "rush bundle --to @hcengineering/tool >/dev/null && cross-env SERVER_SECRET=secret FULLTEXT_URL=http://localhost:4700 ACCOUNTS_URL=http://localhost:3000 TRANSACTOR_URL=ws://localhost:3333 MINIO_ACCESS_KEY=minioadmin MINIO_SECRET_KEY=minioadmin MINIO_ENDPOINT=localhost ACCOUNT_DB_URL=mongodb://localhost:27017 DB_URL=mongodb://localhost:27017 TELEGRAM_DATABASE=telegram-service REKONI_URL=http://localhost:4004 MODEL_VERSION=$(node ../../common/scripts/show_version.js) GIT_REVISION=$(git describe --all --long) QUEUE_CONFIG='localhost:19092' node --expose-gc --max-old-space-size=18000 ./bundle/bundle.js",
24-
"run-local": "rush bundle --to @hcengineering/tool >/dev/null && cross-env SERVER_SECRET=secret FULLTEXT_URL=http://localhost:4702 ACCOUNTS_URL=http://localhost:3000 TRANSACTOR_URL=ws://localhost:3332 STORAGE_CONFIG='datalake|http://huly.local:4030' ACCOUNT_DB_URL=postgresql://[email protected]:26257/defaultdb?sslmode=disable DB_URL=postgresql://[email protected]:26257/defaultdb?sslmode=disable TELEGRAM_DATABASE=telegram-service REKONI_URL=http://localhost:4004 REGION_INFO='cockroach|CockroachDB' MODEL_VERSION=$(node ../../common/scripts/show_version.js) GIT_REVISION=$(git describe --all --long) QUEUE_CONFIG='localhost:19092' node --expose-gc --max-old-space-size=18000 $TOOL_OPT ./bundle/bundle.js",
24+
"run-local": "rush bundle --to @hcengineering/tool >/dev/null && cross-env SERVER_SECRET=secret FULLTEXT_URL=http://localhost:4702 ACCOUNTS_URL=http://localhost:3000 TRANSACTOR_URL=ws://localhost:3332 STORAGE_CONFIG='datalake|http://huly.local:4030' ACCOUNT_DB_URL=postgresql://[email protected]:26257/defaultdb?sslmode=disable DB_URL=postgresql://[email protected]:26257/defaultdb?sslmode=disable TELEGRAM_DATABASE=telegram-service REKONI_URL=http://localhost:4004 REGION_INFO='cockroach|CockroachDB' MODEL_VERSION=$(node ../../common/scripts/show_version.js) GIT_REVISION=$(git describe --all --long) QUEUE_CONFIG='localhost:19092' node --expose-gc --max-old-space-size=18000 $TOOL_OPT ./bundle/bundle.js",
2525
"run-local-brk": "rush bundle --to @hcengineering/tool >/dev/null && cross-env SERVER_SECRET=secret ACCOUNTS_URL=http://localhost:3000 TRANSACTOR_URL=ws://localhost:3333 MINIO_ACCESS_KEY=minioadmin MINIO_SECRET_KEY=minioadmin MINIO_ENDPOINT=localhost ACCOUNT_DB_URL=mongodb://localhost:27017 DB_URL=mongodb://localhost:27017 TELEGRAM_DATABASE=telegram-service REKONI_URL=http://localhost:4004 MODEL_VERSION=$(node ../../common/scripts/show_version.js) GIT_REVISION=$(git describe --all --long) node --inspect-brk --enable-source-maps --max-old-space-size=18000 ./bundle/bundle.js",
2626
"run": "rush bundle --to @hcengineering/tool >/dev/null && cross-env node --max-old-space-size=8000 ./bundle/bundle.js",
2727
"upgrade-mongo": "rushx run-local-mongo upgrade-workspace -- $1",
@@ -60,6 +60,7 @@
6060
"dependencies": {
6161
"@elastic/elasticsearch": "^7.17.14",
6262
"@hcengineering/account": "^0.6.0",
63+
"@hcengineering/account-service": "^0.6.0",
6364
"@hcengineering/workspace-service": "^0.6.0",
6465
"@hcengineering/attachment": "^0.6.14",
6566
"@hcengineering/calendar": "^0.6.24",

dev/tool/src/db.ts

Lines changed: 340 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,8 @@ import {
88
findFullSocialIdBySocialKey,
99
findPersonBySocialKey,
1010
mergeSpecifiedPersons,
11-
mergeSpecifiedAccounts
11+
mergeSpecifiedAccounts,
12+
createAccount
1213
} from '@hcengineering/account'
1314
import { getFirstName, getLastName } from '@hcengineering/contact'
1415
import {
@@ -23,7 +24,11 @@ import {
2324
type AccountUuid,
2425
parseSocialIdString,
2526
DOMAIN_SPACE,
26-
AccountRole
27+
AccountRole,
28+
generateId,
29+
type WorkspaceDataId,
30+
type WorkspaceUuid,
31+
generateUuid
2732
} from '@hcengineering/core'
2833
import { getMongoClient, getWorkspaceMongoDB } from '@hcengineering/mongo'
2934
import {
@@ -39,6 +44,11 @@ import { type DBDoc } from '@hcengineering/postgres/types/utils'
3944
import { getTransactorEndpoint } from '@hcengineering/server-client'
4045
import { generateToken } from '@hcengineering/server-token'
4146
import { connect } from '@hcengineering/server-tool'
47+
import {
48+
type MongoAccountDB as v6MongoAccountDB,
49+
type Account as OldAccount,
50+
type Workspace as OldWorkspace
51+
} from '@hcengineering/account-service'
4252
import { type MongoClient } from 'mongodb'
4353
import type postgres from 'postgres'
4454
import { type Row } from 'postgres'
@@ -1138,3 +1148,331 @@ export async function ensureGlobalPersonsForLocalAccounts (
11381148
pg.close()
11391149
}
11401150
}
1151+
1152+
export async function migrateTrustedV6Accounts (
1153+
ctx: MeasureMetricsContext,
1154+
accountDB: AccountDB,
1155+
mongoDb: v6MongoAccountDB,
1156+
dryRun: boolean,
1157+
skipWorkspaces: Set<string>
1158+
): Promise<void> {
1159+
// Mapping between <ObjectId, UUID>
1160+
const accountsIdToUuid: Record<string, AccountUuid> = {}
1161+
// Mapping between <email, UUID>
1162+
const accountsEmailToUuid: Record<string, AccountUuid> = {}
1163+
// Mapping between <OldId, UUID>
1164+
const workspacesIdToUuid: Record<WorkspaceDataId, WorkspaceUuid> = {}
1165+
1166+
console.log('Migrating accounts database...')
1167+
let accountsProcessed = 0
1168+
const accountsCursor = mongoDb.account.findCursor({})
1169+
try {
1170+
while (await accountsCursor.hasNext()) {
1171+
const account = await accountsCursor.next()
1172+
if (account == null) {
1173+
break
1174+
}
1175+
1176+
try {
1177+
const accountUuid = await migrateAccount(account, accountDB, dryRun)
1178+
if (accountUuid == null) {
1179+
console.log('Account not migrated', account)
1180+
continue
1181+
}
1182+
accountsIdToUuid[account._id.toString()] = accountUuid
1183+
accountsEmailToUuid[account.email] = accountUuid
1184+
1185+
accountsProcessed++
1186+
if (accountsProcessed % 100 === 0) {
1187+
console.log('Processed accounts:', accountsProcessed)
1188+
}
1189+
} catch (err: any) {
1190+
console.log('Failed to migrate account', account._id, account.email, err)
1191+
}
1192+
}
1193+
} catch (err: any) {
1194+
console.log('Failed to migrate accounts', err)
1195+
} finally {
1196+
await accountsCursor.close()
1197+
}
1198+
1199+
console.log('Total accounts processed:', accountsProcessed)
1200+
1201+
let processedWorkspaces = 0
1202+
const workspacesCursor = mongoDb.workspace.findCursor({})
1203+
try {
1204+
while (await workspacesCursor.hasNext()) {
1205+
const workspace = await workspacesCursor.next()
1206+
if (workspace == null) {
1207+
break
1208+
}
1209+
1210+
if (
1211+
skipWorkspaces.has(workspace.workspace) ||
1212+
(workspace.workspaceUrl != null && skipWorkspaces.has(workspace.workspaceUrl))
1213+
) {
1214+
console.log('Skipping workspace', workspace.workspace, workspace.workspaceUrl)
1215+
continue
1216+
}
1217+
1218+
try {
1219+
const workspaceUuid = await migrateWorkspace(
1220+
workspace,
1221+
accountDB,
1222+
accountsIdToUuid,
1223+
accountsEmailToUuid,
1224+
dryRun
1225+
)
1226+
1227+
if (workspaceUuid !== undefined) {
1228+
workspacesIdToUuid[workspace.workspace] = workspaceUuid
1229+
}
1230+
processedWorkspaces++
1231+
if (processedWorkspaces % 100 === 0) {
1232+
console.log('Processed workspaces:', processedWorkspaces)
1233+
}
1234+
} catch (err: any) {
1235+
console.log('Failed to migrate workspace', workspace.workspaceUrl, workspace.workspace, err)
1236+
}
1237+
}
1238+
} catch (err: any) {
1239+
console.log('Failed to migrate workspaces', err)
1240+
} finally {
1241+
await workspacesCursor.close()
1242+
}
1243+
1244+
console.log('Total workspaces processed:', processedWorkspaces)
1245+
console.log('Total workspaces created/ensured:', Object.values(workspacesIdToUuid).length)
1246+
1247+
let invitesProcessed = 0
1248+
const invitesCursor = mongoDb.invite.findCursor({})
1249+
try {
1250+
while (await invitesCursor.hasNext()) {
1251+
const invite = await invitesCursor.next()
1252+
if (invite == null) {
1253+
break
1254+
}
1255+
1256+
try {
1257+
const workspaceUuid = workspacesIdToUuid[invite.workspace.name]
1258+
if (workspaceUuid === undefined) {
1259+
console.log('No workspace with id', invite.workspace.name, 'found for invite', invite._id)
1260+
continue
1261+
}
1262+
1263+
const existing = await accountDB.invite.findOne({ migratedFrom: invite._id.toString() })
1264+
if (existing != null) {
1265+
continue
1266+
}
1267+
1268+
const inviteRecord = {
1269+
migratedFrom: invite._id.toString(),
1270+
workspaceUuid,
1271+
expiresOn: invite.exp,
1272+
emailPattern: invite.emailMask,
1273+
remainingUses: invite.limit,
1274+
role: invite.role ?? AccountRole.User
1275+
}
1276+
1277+
if (!dryRun) {
1278+
await accountDB.invite.insertOne(inviteRecord)
1279+
} else {
1280+
console.log('Creating invite record', inviteRecord)
1281+
}
1282+
1283+
invitesProcessed++
1284+
if (invitesProcessed % 100 === 0) {
1285+
console.log('Processed invites:', invitesProcessed)
1286+
}
1287+
} catch (err: any) {
1288+
console.log('Failed to migrate invite', invite._id, err)
1289+
}
1290+
}
1291+
} catch (err: any) {
1292+
console.log('Failed to migrate invites', err)
1293+
} finally {
1294+
await invitesCursor.close()
1295+
}
1296+
1297+
console.log('Total invites processed:', invitesProcessed)
1298+
}
1299+
1300+
async function migrateAccount (
1301+
account: OldAccount,
1302+
accountDB: AccountDB,
1303+
dryRun = true
1304+
): Promise<AccountUuid | undefined> {
1305+
const primaryKey: SocialKey = {
1306+
type: SocialIdType.EMAIL,
1307+
value: account.email
1308+
}
1309+
1310+
let personUuid: PersonUuid
1311+
const verified = account.confirmed === true ? { verifiedOn: Date.now() } : {}
1312+
1313+
const existing = await accountDB.socialId.findOne(primaryKey)
1314+
if (existing == null) {
1315+
// Create new global person
1316+
const personRecord = {
1317+
firstName: account.first,
1318+
lastName: account.last
1319+
}
1320+
1321+
if (!dryRun) {
1322+
personUuid = await accountDB.person.insertOne(personRecord)
1323+
} else {
1324+
console.log('Creating person record', personRecord)
1325+
personUuid = generateUuid() as PersonUuid
1326+
}
1327+
1328+
const socialIdRecord = {
1329+
...primaryKey,
1330+
personUuid,
1331+
...verified
1332+
}
1333+
1334+
if (!dryRun) {
1335+
await accountDB.socialId.insertOne(socialIdRecord)
1336+
} else {
1337+
console.log('Creating social id record', socialIdRecord)
1338+
}
1339+
1340+
if (!dryRun) {
1341+
await createAccount(accountDB, personUuid, account.confirmed, false, account.createdOn)
1342+
} else {
1343+
console.log('Creating account record', { personUuid, confirmed: account.confirmed })
1344+
}
1345+
1346+
if (account.hash != null && account.salt != null) {
1347+
if (!dryRun) {
1348+
await accountDB.setPassword(personUuid as AccountUuid, account.hash, account.salt)
1349+
} else {
1350+
console.log('Updating account password', { personUuid })
1351+
}
1352+
}
1353+
} else {
1354+
personUuid = existing.personUuid
1355+
1356+
// if there's no existing account, create a new one
1357+
const existingAcc = await accountDB.account.findOne({ uuid: personUuid as AccountUuid })
1358+
if (existingAcc == null) {
1359+
if (!dryRun) {
1360+
await createAccount(accountDB, personUuid, account.confirmed, false, account.createdOn)
1361+
} else {
1362+
console.log('Creating account record', { personUuid, confirmed: account.confirmed })
1363+
}
1364+
1365+
if (account.hash != null && account.salt != null) {
1366+
if (!dryRun) {
1367+
await accountDB.setPassword(personUuid as AccountUuid, account.hash, account.salt)
1368+
} else {
1369+
console.log('Updating account password', { personUuid })
1370+
}
1371+
}
1372+
}
1373+
}
1374+
1375+
return personUuid as AccountUuid
1376+
}
1377+
1378+
async function migrateWorkspace (
1379+
workspace: OldWorkspace,
1380+
accountDB: AccountDB,
1381+
accountsIdToUuid: Record<string, AccountUuid>,
1382+
accountsEmailToUuid: Record<string, AccountUuid>,
1383+
dryRun = true
1384+
): Promise<WorkspaceUuid | undefined> {
1385+
if (workspace.workspaceUrl == null) {
1386+
console.log('No workspace url, skipping', workspace.workspace)
1387+
return
1388+
}
1389+
1390+
const createdBy = workspace.createdBy !== undefined ? accountsEmailToUuid[workspace.createdBy] : undefined
1391+
if (createdBy === undefined) {
1392+
console.log('No account found for workspace', workspace.workspace, 'created by', workspace.createdBy)
1393+
}
1394+
1395+
const existingByUrl = await accountDB.workspace.findOne({ url: workspace.workspaceUrl })
1396+
const existingByUuid = await accountDB.workspace.findOne({ uuid: workspace.uuid })
1397+
1398+
let workspaceUuid: WorkspaceUuid
1399+
1400+
if (existingByUuid == null) {
1401+
let url = workspace.workspaceUrl
1402+
if (existingByUrl != null) {
1403+
// generate new url
1404+
url = `${url}-${generateId('-')}`
1405+
console.log('Generating new url', url)
1406+
}
1407+
1408+
const workspaceRecord = {
1409+
uuid: workspace.uuid,
1410+
name: workspace.workspaceName,
1411+
url,
1412+
dataId: workspace.workspace,
1413+
branding: workspace.branding,
1414+
region: workspace.region,
1415+
createdBy,
1416+
billingAccount: createdBy,
1417+
createdOn: workspace.createdOn ?? Date.now()
1418+
}
1419+
1420+
if (!dryRun) {
1421+
workspaceUuid = await accountDB.workspace.insertOne(workspaceRecord)
1422+
} else {
1423+
console.log('Creating workspace record', workspaceRecord)
1424+
workspaceUuid = generateUuid() as WorkspaceUuid
1425+
}
1426+
} else {
1427+
workspaceUuid = existingByUuid.uuid
1428+
}
1429+
1430+
const existingStatus = await accountDB.workspaceStatus.findOne({ workspaceUuid })
1431+
1432+
if (existingStatus == null) {
1433+
const statusRecord = {
1434+
workspaceUuid,
1435+
mode: workspace.mode,
1436+
processingProgress: workspace.progress !== undefined ? Math.floor(workspace.progress) : undefined,
1437+
versionMajor: workspace.version?.major,
1438+
versionMinor: workspace.version?.minor,
1439+
versionPatch: workspace.version?.patch,
1440+
lastProcessingTime: workspace.lastProcessingTime,
1441+
lastVisit: workspace.lastVisit,
1442+
isDisabled: workspace.disabled,
1443+
processingAttempts: workspace.attempts,
1444+
processingMessage: workspace.message,
1445+
backupInfo: workspace.backupInfo
1446+
}
1447+
1448+
if (!dryRun) {
1449+
await accountDB.workspaceStatus.insertOne(statusRecord)
1450+
} else {
1451+
console.log('Creating workspace status record', statusRecord)
1452+
}
1453+
}
1454+
1455+
const uniqueAccounts = Array.from(new Set((workspace.accounts ?? []).map((it) => it.toString())))
1456+
const existingMembers = new Set((await accountDB.getWorkspaceMembers(workspaceUuid)).map((mi) => mi.person))
1457+
for (const member of uniqueAccounts) {
1458+
const accountUuid = accountsIdToUuid[member]
1459+
1460+
if (accountUuid === undefined) {
1461+
console.log('No account found for workspace', workspace.workspace, 'member', member)
1462+
continue
1463+
}
1464+
1465+
if (existingMembers.has(accountUuid)) {
1466+
continue
1467+
}
1468+
1469+
if (!dryRun) {
1470+
// Actual roles are being set in workspace migration
1471+
await accountDB.assignWorkspace(accountUuid, workspaceUuid, AccountRole.Guest)
1472+
} else {
1473+
console.log('Assigning account', member, accountUuid, 'to workspace', workspaceUuid)
1474+
}
1475+
}
1476+
1477+
return workspaceUuid
1478+
}

0 commit comments

Comments
 (0)