dani-garcia/vaultwarden · error

Grantor user not found

Error message

Grantor user not found

What it means

In the same timeout job with mail enabled, the grantor user is loaded with User::find_by_uuid(&emer.grantor_uuid, &conn).await.expect("Grantor user not found") to address the notification email. If the grantor account was deleted while the emergency_access row survived, the lookup returns None and the job task panics, stopping the job for all remaining rows until restart.

Solutions

  1. Purge or fix orphaned rows (back up first): DELETE FROM emergency_access WHERE grantor_uuid NOT IN (SELECT uuid FROM users)
  2. Restart Vaultwarden to restart the job task
  3. Code fix: treat a missing user as a skip, not a panic

Example fix

// before
let grantor_user = User::find_by_uuid(&emer.grantor_uuid, &conn).await.expect("Grantor user not found");
// after
let Some(grantor_user) = User::find_by_uuid(&emer.grantor_uuid, &conn).await else {
    warn!("Grantor {} missing for emergency access {}; skipping", emer.grantor_uuid, emer.uuid);
    continue;
};
Defensive patterns

Strategy: validation

Validate before calling

-- Detect grantor orphans before the job does
SELECT ea.uuid, ea.grantor_uuid FROM emergency_access ea
LEFT JOIN users u ON u.uuid = ea.grantor_uuid
WHERE u.uuid IS NULL;

Prevention

When it happens

Trigger: A recovery timing out for an emergency access whose grantor_uuid no longer matches a user row — deletion without cleanup, corrupted uuid values, or partial restores.

Common situations: Admin-deleted or purged accounts with lingering emergency access; database restores that dropped users but kept emergency_access rows.

Related errors


AI-assisted analysis of dani-garcia/vaultwarden@0cefa4cca7 (2026-08-16). Data as JSON: /api/errors/110167e934f7c5c8. Report an issue: GitHub.

Appendix: source

Thrown at src/api/core/emergency_access.rs:750

            debug!("No emergency request timeout to approve");
        }

        let now = Utc::now().naive_utc();
        for mut emer in emergency_access_list {
            // The find_all_recoveries_initiated already checks if the recovery_initiated_at is not null (None)
            let recovery_allowed_at =
                emer.recovery_initiated_at.unwrap() + TimeDelta::try_days(i64::from(emer.wait_time_days)).unwrap();
            if recovery_allowed_at.le(&now) {
                // Only update the access status
                // Updating the whole record could cause issues when the emergency_notification_reminder_job is also active
                emer.update_access_status_and_save(EmergencyAccessStatus::RecoveryApproved as i32, &now, &conn)
                    .await
                    .expect("Unable to update emergency access status");

                if CONFIG.mail_enabled() {
                    // get grantor user to send Accepted email
                    let grantor_user =
                        User::find_by_uuid(&emer.grantor_uuid, &conn).await.expect("Grantor user not found");

                    // get grantee user to send Accepted email
                    let grantee_user =
                        User::find_by_uuid(&emer.grantee_uuid.clone().expect("Grantee user invalid"), &conn)
                            .await
                            .expect("Grantee user not found");

                    mail::send_emergency_access_recovery_timed_out(
                        &grantor_user.email,
                        &grantee_user.name,
                        emer.get_type_as_str(),
                    )
                    .await
                    .expect("Error on sending email");

                    mail::send_emergency_access_recovery_approved(&grantee_user.email, &grantor_user.name)
                        .await
                        .expect("Error on sending email");

View on GitHub (pinned to 0cefa4cca7)