Skip to content

Commit a352827

Browse files
authored
Merge pull request #1824 from CVEProject/af-1730
Resolves #1730, Fix database inconsistency with account cleanup
2 parents c0b4586 + 3b099f6 commit a352827

1 file changed

Lines changed: 122 additions & 97 deletions

File tree

src/repositories/baseUserRepository.js

Lines changed: 122 additions & 97 deletions
Original file line numberDiff line numberDiff line change
@@ -419,96 +419,124 @@ class BaseUserRepository extends BaseRepository {
419419
const { createAuditLogEntry } = require('./baseOrgRepositoryHelpers')
420420
const registryOrg = await baseOrgRepository.getOrgObject(orgShortname, false, options)
421421
const originalRegistryOrg = registryOrg.toObject()
422+
422423
const legacyUser = await legacyUserRepo.findOneByUserNameAndOrgUUID(username, registryOrg.UUID, null, options)
423-
const registryUser = await this.findOneByUsernameAndOrgShortname(username, orgShortname, options, true) // WE always want the registry user
424+
const registryUser = await this.findOneByUsernameAndOrgShortname(username, orgShortname, options, true)
425+
426+
if (!registryUser && !legacyUser) {
427+
throw new Error('User not found')
428+
}
424429

425-
registryUser.username = incomingParameters?.new_username ?? registryUser.username
426-
legacyUser.username = incomingParameters?.new_username ?? legacyUser.username
430+
// Safely assign usernames defensively
431+
if (registryUser) {
432+
registryUser.username = incomingParameters?.new_username ?? registryUser.username
433+
}
434+
if (legacyUser) {
435+
legacyUser.username = incomingParameters?.new_username ?? legacyUser.username
436+
}
427437

428438
if (incomingParameters?.active != null) {
429439
const isConsideredActive = incomingParameters.active === true || String(incomingParameters.active).toLowerCase() === 'true'
430-
registryUser.status = isConsideredActive ? 'active' : 'inactive'
431-
legacyUser.active = incomingParameters.active ?? legacyUser.active
440+
if (registryUser) {
441+
registryUser.status = isConsideredActive ? 'active' : 'inactive'
442+
}
443+
if (legacyUser) {
444+
legacyUser.active = incomingParameters.active ?? legacyUser.active
445+
}
432446
}
433447

434448
['name.last', 'name.first', 'name.middle', 'name.suffix'].forEach(field => {
435-
_.set(registryUser, field, _.get(incomingParameters, field, _.get(registryUser, field, '')))
436-
_.set(legacyUser, field, _.get(incomingParameters, field, _.get(legacyUser, field, '')))
449+
if (registryUser) _.set(registryUser, field, _.get(incomingParameters, field, _.get(registryUser, field, '')))
450+
if (legacyUser) _.set(legacyUser, field, _.get(incomingParameters, field, _.get(legacyUser, field, '')))
437451
})
438452

453+
// Get the UUID from whichever user object actually exists
454+
const userUUID = registryUser?.UUID ?? legacyUser?.UUID
455+
439456
const rolesToAdd = _.flattenDeep(_.compact(_.get(incomingParameters, 'active_roles.add')))
440457
const rolesToRemove = _.flattenDeep(_.compact(_.get(incomingParameters, 'active_roles.remove')))
441-
if (rolesToRemove.includes('ADMIN')) {
458+
459+
if (rolesToRemove.includes('ADMIN') && userUUID) {
442460
if (Array.isArray(registryOrg.admins)) {
443-
registryOrg.admins.pull(registryUser.UUID)
461+
registryOrg.admins.pull(userUUID)
444462
}
445463
}
446464

447-
if (rolesToAdd.includes('ADMIN') && !incomingParameters?.org_short_name) {
448-
// Use the already fetched registryOrg instead of querying again
465+
if (rolesToAdd.includes('ADMIN') && !incomingParameters?.org_short_name && userUUID) {
449466
if (!Array.isArray(registryOrg.admins)) {
450467
registryOrg.admins = []
451468
}
452-
registryOrg.admins.addToSet(registryUser.UUID)
469+
registryOrg.admins.addToSet(userUUID)
453470
}
454471

455-
const initialRoles = legacyUser.authority?.active_roles ?? []
472+
// Handle roles calculation fallback
473+
const initialRoles = legacyUser?.authority?.active_roles ?? []
456474
const finalRoles = [...new Set([...initialRoles, ...rolesToAdd])].filter(role => !rolesToRemove.includes(role))
457-
registryUser.role = finalRoles[0] ?? ''
458-
_.set(legacyUser, 'authority.active_roles', finalRoles)
459475

460-
if (incomingParameters?.org_short_name) {
461-
// Remove us from the old users Array
476+
if (registryUser) {
477+
registryUser.role = finalRoles[0] ?? ''
478+
}
479+
if (legacyUser) {
480+
_.set(legacyUser, 'authority.active_roles', finalRoles)
481+
}
482+
483+
if (incomingParameters?.org_short_name && userUUID) {
462484
if (Array.isArray(registryOrg.users)) {
463-
registryOrg.users.pull(registryUser.UUID)
485+
registryOrg.users.pull(userUUID)
464486
}
465-
if (registryOrg.admins && registryOrg.admins.includes(registryUser.UUID)) {
466-
registryOrg.admins.pull(registryUser.UUID)
487+
if (registryOrg.admins && registryOrg.admins.includes(userUUID)) {
488+
registryOrg.admins.pull(userUUID)
467489
}
468-
// Add us to the new org (this is a genuine cross-org migration, so we must fetch the new org)
490+
469491
const newOrg = await baseOrgRepository.getOrgObject(incomingParameters.org_short_name)
470492
const originalNewOrg = newOrg.toObject()
471493
if (!Array.isArray(newOrg.users)) {
472494
newOrg.users = []
473495
}
474-
newOrg.users.addToSet(registryUser.UUID)
496+
newOrg.users.addToSet(userUUID)
475497

476-
if (registryUser.role.includes('ADMIN')) {
498+
const isUserAdmin = registryUser?.role?.includes('ADMIN') || finalRoles.includes('ADMIN')
499+
if (isUserAdmin) {
477500
if (!Array.isArray(newOrg.admins)) {
478501
newOrg.admins = []
479502
}
480-
newOrg.admins.addToSet(registryUser.UUID)
503+
newOrg.admins.addToSet(userUUID)
481504
}
482505

483-
legacyUser.org_UUID = newOrg.UUID
506+
if (legacyUser) {
507+
legacyUser.org_UUID = newOrg.UUID
508+
}
484509
await newOrg.save(options)
485510
if (requestingUserUUID) {
486511
await createAuditLogEntry(newOrg, originalNewOrg, requestingUserUUID, options)
487512
}
488513
}
489514

490-
delete registryUser.role
491-
// Single unified save for the primary org at the end
515+
if (registryUser) {
516+
delete registryUser.role
517+
}
518+
492519
await registryOrg.save(options)
493520
if (requestingUserUUID) {
494521
await createAuditLogEntry(registryOrg, originalRegistryOrg, requestingUserUUID, options)
495522
}
496523

497-
await legacyUser.save(options)
498-
await registryUser.save(options)
524+
// Save only records that exist
525+
if (legacyUser) await legacyUser.save(options)
526+
if (registryUser) await registryUser.save(options)
499527

500528
if (!isRegistryObject) {
529+
if (!legacyUser) throw new Error('Legacy record missing; cannot return legacy format.')
501530
const plainJavascriptLegacyUser = legacyUser.toObject()
502-
legacyUser.role = finalRoles[0] ?? ''
531+
plainJavascriptLegacyUser.role = finalRoles[0] ?? ''
503532
delete plainJavascriptLegacyUser.__v
504533
delete plainJavascriptLegacyUser._id
505534
delete plainJavascriptLegacyUser.secret
506-
// return deepRemoveEmpty(plainJavascriptLegacyUser)
507535
return plainJavascriptLegacyUser
508536
}
509537

538+
if (!registryUser) throw new Error('Registry record missing; cannot return registry format.')
510539
const plainJavascriptRegistryUser = registryUser.toObject()
511-
// Remove private things
512540
delete plainJavascriptRegistryUser.__v
513541
delete plainJavascriptRegistryUser._id
514542
delete plainJavascriptRegistryUser.__t
@@ -529,117 +557,101 @@ class BaseUserRepository extends BaseRepository {
529557
async updateUserFull (identifier, incomingUser, options = {}, isRegistryObject = true, requestingUserUUID = null) {
530558
const legacyUserRepo = new UserRepository()
531559

532-
// Find registry user by UUID
533560
const registryUser = await this.findUserByUUID(identifier, options)
534-
if (!registryUser) {
535-
throw new Error('Registry user not found')
536-
}
537-
538-
// Find legacy user
539561
const legacyUser = await legacyUserRepo.findOneByUUID(identifier)
540-
if (!legacyUser) {
541-
throw new Error('Legacy user not found')
562+
563+
// Fail only if completely missing everywhere
564+
if (!registryUser && !legacyUser) {
565+
throw new Error('User not found in any repository')
542566
}
543567

544568
const { ...incomingUserBody } = incomingUser
545-
let legacyObjectRaw
546-
let registryObjectRaw
547-
548-
if (!isRegistryObject) {
549-
legacyObjectRaw = incomingUserBody
550-
registryObjectRaw = this.convertLegacyToRegistry(incomingUserBody)
551-
} else {
552-
registryObjectRaw = incomingUserBody
553-
legacyObjectRaw = this.convertRegistryToLegacy(incomingUserBody)
554-
}
569+
const legacyObjectRaw = isRegistryObject ? this.convertRegistryToLegacy(incomingUserBody) : incomingUserBody
570+
const registryObjectRaw = isRegistryObject ? incomingUserBody : this.convertLegacyToRegistry(incomingUserBody)
555571

556572
const protectedFieldsRegistry = ['_id', 'UUID', '__v', 'secret', 'created', 'last_updated']
557573
const protectedFieldsLegacy = ['_id', 'UUID', '__v', 'secret', 'time', 'org_UUID']
558574

559-
const updatedRegistryUser = registryUser.overwrite(_.mergeWith(_.pick(registryUser.toObject(), protectedFieldsRegistry), _.omit(registryObjectRaw, protectedFieldsRegistry), skipNulls))
560-
const updatedLegacyUser = legacyUser.overwrite(_.mergeWith(_.pick(legacyUser.toObject(), protectedFieldsLegacy), _.omit(legacyObjectRaw, protectedFieldsLegacy), skipNulls))
575+
let updatedRegistryUser = null
576+
let updatedLegacyUser = null
561577

562-
if (updatedRegistryUser.status !== 'active') {
563-
updatedRegistryUser.status = 'inactive'
564-
updatedLegacyUser.active = false
565-
} else {
566-
updatedLegacyUser.active = true
578+
if (registryUser) {
579+
updatedRegistryUser = registryUser.overwrite(_.mergeWith(_.pick(registryUser.toObject(), protectedFieldsRegistry), _.omit(registryObjectRaw, protectedFieldsRegistry), skipNulls))
580+
if (updatedRegistryUser.status !== 'active') {
581+
updatedRegistryUser.status = 'inactive'
582+
}
583+
}
584+
585+
if (legacyUser) {
586+
updatedLegacyUser = legacyUser.overwrite(_.mergeWith(_.pick(legacyUser.toObject(), protectedFieldsLegacy), _.omit(legacyObjectRaw, protectedFieldsLegacy), skipNulls))
587+
// Align status from incoming payload or resolved registry state
588+
const targetStatus = registryUser ? updatedRegistryUser.status : (isRegistryObject ? registryObjectRaw.status : 'active')
589+
updatedLegacyUser.active = (targetStatus === 'active')
567590
}
568591

569592
try {
570593
if (incomingUser.org_short_name) {
571594
const baseOrgRepository = new BaseOrgRepository()
572595
const { createAuditLogEntry } = require('./baseOrgRepositoryHelpers')
573-
const currentOrgUUID = legacyUser.org_UUID
574-
const currentOrg = await baseOrgRepository.findOneByUUID(currentOrgUUID)
575-
const originalCurrentOrg = currentOrg.toObject()
596+
597+
// Grab current org UUID using whichever document is real
598+
const currentOrgUUID = legacyUser ? legacyUser.org_UUID : registryUser?.org_UUID // Fallback if schema supports it
599+
const currentOrg = currentOrgUUID ? await baseOrgRepository.findOneByUUID(currentOrgUUID) : null
576600
const newOrg = await baseOrgRepository.findOneByShortName(incomingUser.org_short_name)
577-
const originalNewOrg = newOrg.toObject()
578601

579602
if (!newOrg) {
580603
throw new Error(`Organization ${incomingUser.org_short_name} not found`)
581604
}
582605

583-
// 1. Remove user from old org's users list
584-
if (Array.isArray(currentOrg.users)) {
585-
currentOrg.users.pull(identifier)
586-
}
587-
588-
// 2. Remove user from old org's admins list (if present)
589-
if (currentOrg.admins && currentOrg.admins.includes(identifier)) {
590-
currentOrg.admins.pull(identifier)
606+
// Clean up old org if found
607+
if (currentOrg) {
608+
const originalCurrentOrg = currentOrg.toObject()
609+
if (Array.isArray(currentOrg.users)) currentOrg.users.pull(identifier)
610+
if (currentOrg.admins && currentOrg.admins.includes(identifier)) currentOrg.admins.pull(identifier)
611+
await currentOrg.save(options)
612+
if (requestingUserUUID) await createAuditLogEntry(currentOrg, originalCurrentOrg, requestingUserUUID, options)
591613
}
592614

593-
// 3. Add user to new org's users list
594-
if (!Array.isArray(newOrg.users)) {
595-
newOrg.users = []
596-
}
615+
// Setup new org
616+
const originalNewOrg = newOrg.toObject()
617+
if (!Array.isArray(newOrg.users)) newOrg.users = []
597618
newOrg.users.addToSet(identifier)
598619

599-
// 4. Add user to new org's admins list (if they are an admin)
600-
const isAdmin = updatedRegistryUser.role === 'ADMIN' || (updatedLegacyUser.authority && updatedLegacyUser.authority.active_roles && updatedLegacyUser.authority.active_roles.includes('ADMIN'))
601-
620+
const isAdmin = updatedRegistryUser?.role === 'ADMIN' || (updatedLegacyUser?.authority?.active_roles?.includes('ADMIN'))
602621
if (isAdmin) {
603622
if (!Array.isArray(newOrg.admins)) {
604623
newOrg.admins = []
605624
}
606625
newOrg.admins.addToSet(identifier)
607626
}
608627

609-
// 5. Update user's org_UUID
610-
updatedLegacyUser.org_UUID = newOrg.UUID
628+
if (updatedLegacyUser) {
629+
updatedLegacyUser.org_UUID = newOrg.UUID
630+
}
611631

612-
// Save org changes
613-
await currentOrg.save(options)
614632
await newOrg.save(options)
615-
616633
if (requestingUserUUID) {
617-
await createAuditLogEntry(currentOrg, originalCurrentOrg, requestingUserUUID, options)
618634
await createAuditLogEntry(newOrg, originalNewOrg, requestingUserUUID, options)
619635
}
620636
}
621637

622-
await updatedLegacyUser.save(options)
623-
await updatedRegistryUser.save(options)
638+
// Conditionally save records
639+
if (updatedLegacyUser) await updatedLegacyUser.save(options)
640+
if (updatedRegistryUser) await updatedRegistryUser.save(options)
624641
} catch (error) {
625642
throw new Error('Failed to update user: ' + error.message)
626643
}
627644

628645
if (!isRegistryObject) {
646+
if (!updatedLegacyUser) throw new Error('Legacy record missing; cannot output legacy format.')
629647
const plain = updatedLegacyUser.toObject()
630-
delete plain._id
631-
delete plain.__v
632-
delete plain.secret
648+
delete plain._id; delete plain.__v; delete plain.secret
633649
return plain
634650
}
635651

636-
// Retrieve updated registry user
652+
if (!updatedRegistryUser) throw new Error('Registry record missing; cannot output registry format.')
637653
const plainJsRegistryUser = updatedRegistryUser.toObject()
638-
delete plainJsRegistryUser._id
639-
delete plainJsRegistryUser.__v
640-
delete plainJsRegistryUser.secret
641-
delete plainJsRegistryUser.authority
642-
654+
delete plainJsRegistryUser._id; delete plainJsRegistryUser.__v; delete plainJsRegistryUser.secret; delete plainJsRegistryUser.authority
643655
return plainJsRegistryUser
644656
}
645657

@@ -661,12 +673,25 @@ class BaseUserRepository extends BaseRepository {
661673
const legUser = await legacyUserRepo.findOneByUserNameAndOrgUUID(username, legOrgUUID, null, options)
662674
const regUser = await this.findOneByUsernameAndOrgShortname(username, orgShortName, options, true)
663675

676+
// Fail ONLY if the user is completely missing from both collections
677+
if (!legUser && !regUser) {
678+
throw new Error('User not found in registry or legacy system.')
679+
}
680+
664681
const randomKey = cryptoRandomString({ length: getConstants().CRYPTO_RANDOM_STRING_LENGTH })
665682
const secret = await argon2.hash(randomKey)
666-
legUser.secret = secret
667-
regUser.secret = secret
668-
await legUser.save(options)
669-
await regUser.save(options)
683+
684+
// Defensively update legacy if present
685+
if (legUser) {
686+
legUser.secret = secret
687+
await legUser.save(options)
688+
}
689+
690+
// Defensively update registry if present
691+
if (regUser) {
692+
regUser.secret = secret
693+
await regUser.save(options)
694+
}
670695

671696
return randomKey
672697
}

0 commit comments

Comments
 (0)