{"record":{"id":"ef450a6da31d9858","repo":"dani-garcia/vaultwarden","slug":"grantee-user-invalid","errorCode":null,"errorMessage":"Grantee user invalid","messagePattern":"Grantee user invalid","errorType":"panic","errorClass":null,"httpStatus":null,"severity":"error","filePath":"src/api/core/emergency_access.rs","lineNumber":754,"sourceCode":"        for mut emer in emergency_access_list {\n            // The find_all_recoveries_initiated already checks if the recovery_initiated_at is not null (None)\n            let recovery_allowed_at =\n                emer.recovery_initiated_at.unwrap() + TimeDelta::try_days(i64::from(emer.wait_time_days)).unwrap();\n            if recovery_allowed_at.le(&now) {\n                // Only update the access status\n                // Updating the whole record could cause issues when the emergency_notification_reminder_job is also active\n                emer.update_access_status_and_save(EmergencyAccessStatus::RecoveryApproved as i32, &now, &conn)\n                    .await\n                    .expect(\"Unable to update emergency access status\");\n\n                if CONFIG.mail_enabled() {\n                    // get grantor user to send Accepted email\n                    let grantor_user =\n                        User::find_by_uuid(&emer.grantor_uuid, &conn).await.expect(\"Grantor user not found\");\n\n                    // get grantee user to send Accepted email\n                    let grantee_user =\n                        User::find_by_uuid(&emer.grantee_uuid.clone().expect(\"Grantee user invalid\"), &conn)\n                            .await\n                            .expect(\"Grantee user not found\");\n\n                    mail::send_emergency_access_recovery_timed_out(\n                        &grantor_user.email,\n                        &grantee_user.name,\n                        emer.get_type_as_str(),\n                    )\n                    .await\n                    .expect(\"Error on sending email\");\n\n                    mail::send_emergency_access_recovery_approved(&grantee_user.email, &grantor_user.name)\n                        .await\n                        .expect(\"Error on sending email\");\n                }\n            }\n        }\n    } else {","sourceCodeStart":736,"sourceCodeEnd":772,"githubUrl":"https://github.com/dani-garcia/vaultwarden/blob/0cefa4cca7c9f2a5579dd290f78193b543818c51/src/api/core/emergency_access.rs#L736-L772","documentation":"emer.grantee_uuid.clone().expect(\"Grantee user invalid\") in the timeout job assumes every recovery-initiated record carries a grantee uuid. grantee_uuid is only populated when the invite is accepted, and the job's query is meant to return only accepted records — so None here indicates state corruption: status advanced without accept_invite, or manual row edits.","triggerScenarios":"A row with recovery_initiated_at set but grantee_uuid NULL — usually from direct DB manipulation or a race/bug that advanced the status before acceptance.","commonSituations":"Hand-edited or partially restored databases; forks altering the emergency access state machine; migration glitches.","solutions":["Inspect and repair the offending rows: set a valid grantee_uuid or reset status/recovery_initiated_at","Restart the service so the job resumes","Code fix: skip rows with a missing grantee instead of panicking"],"exampleFix":"// before\nUser::find_by_uuid(&emer.grantee_uuid.clone().expect(\"Grantee user invalid\"), &conn)\n// after\nlet Some(grantee_uuid) = emer.grantee_uuid.clone() else {\n    warn!(\"Emergency access {} has no grantee uuid; skipping\", emer.uuid);\n    continue;\n};\nUser::find_by_uuid(&grantee_uuid, &conn)","handlingStrategy":"validation","validationCode":"-- Rows that will trip the job: recovery initiated but no grantee recorded\nSELECT uuid, status, grantee_uuid, recovery_initiated_at\nFROM emergency_access\nWHERE recovery_initiated_at IS NOT NULL AND grantee_uuid IS NULL;","typeGuard":null,"tryCatchPattern":null,"preventionTips":["Never advance emergency_access status via raw SQL","Test restores against the full schema including dependent rows","Monitor job panics as an early signal of state corruption"],"tags":["rust","emergency-access","database","panic","state-corruption"],"backgroundTag":null,"analyzedSha":"0cefa4cca7c9f2a5579dd290f78193b543818c51","analyzedAt":"2026-08-16T07:44:56.102Z","schemaVersion":2},"datasetVersion":"2026-08-16T08:17:34.114Z"}