{"record":{"id":"52bd5091b3598199","repo":"we-promise/sure","slug":"cannot-destroy-demo-monitoring-api-key","errorCode":null,"errorMessage":"Cannot destroy demo monitoring API key","messagePattern":"Cannot destroy demo monitoring API key","errorType":"exception","errorClass":"ActiveRecord::RecordNotDestroyed","httpStatus":null,"severity":"warning","filePath":"app/models/api_key.rb","lineNumber":72,"sourceCode":"  def revoked?\n    revoked_at.present?\n  end\n\n  def expired?\n    expires_at.present? && expires_at < Time.current\n  end\n\n  def key_matches?(plain_key)\n    display_key == plain_key\n  end\n\n  def revoke!\n    raise ActiveRecord::RecordNotDestroyed, \"Cannot revoke demo monitoring API key\" if demo_monitoring_key?\n    update!(revoked_at: Time.current)\n  end\n\n  def delete\n    raise ActiveRecord::RecordNotDestroyed, \"Cannot destroy demo monitoring API key\" if demo_monitoring_key?\n    super\n  end\n\n  def demo_monitoring_key?\n    display_key == DEMO_MONITORING_KEY\n  end\n\n  def update_last_used!\n    update_column(:last_used_at, Time.current)\n  end\n\n  # Get the plain text API key for display (automatically decrypted by Rails)\n  def plain_key\n    display_key\n  end\n\n  # Temporarily store the plain key for creation flow\n  attr_accessor :key","sourceCodeStart":54,"sourceCodeEnd":90,"githubUrl":"https://github.com/we-promise/sure/blob/e69894adb92547273377398c15f45c979cd9416a/app/models/api_key.rb#L54-L90","documentation":"ApiKey#delete overrides the model's delete to raise ActiveRecord::RecordNotDestroyed(\"Cannot destroy demo monitoring API key\") when the record is the seeded demo monitoring key (display_key == DEMO_MONITORING_KEY). destroy goes through delete, so both normal and skip-callbacks destruction paths are blocked. Like revoke!, this protects the demo family's external monitoring credential from being removed piecemeal — the demo teardown (Demo::DataCleaner) is the only sanctioned remover, and it uses delete_all to bypass exactly this guard deliberately.","triggerScenarios":"Running api_key.destroy or api_key.delete on the demo monitoring key from console/script; a generic 'purge old API keys' cleanup task iterating all records; dependent => :destroy on a user being deleted who holds the demo key (Demo::Generator attaches it to the demo admin user) — destroying that user cascades into this raise; factories/tests that accidentally instantiate a key with the demo display_key value.","commonSituations":"Deleting old/unused keys during an audit and catching the demo one; tearing down a demo admin user without running the demo cleaner; test suites with loose factories producing the reserved display_key; copying production data to staging and cleaning it with naive destroy loops.","solutions":["Use the sanctioned teardown: Demo::DataCleaner removes demo records and delete_all's the key, bypassing the guard on purpose","Exclude the key from bulk deletion: ApiKey.visible.each(&:destroy) or next if k.demo_monitoring_key?","When deleting the demo admin user, run demo cleanup first so the cascade never reaches the protected key","In tests, never use DEMO_MONITORING_KEY as a factory value; generate random keys"],"exampleFix":"# before\nuser.api_keys.each(&:destroy)\n# => ActiveRecord::RecordNotDestroyed: Cannot destroy demo monitoring API key\n\n# after\nuser.api_keys.visible.each(&:destroy)        # guard never trips\n# full demo teardown instead of piecemeal destroy:\n# Demo::DataCleaner.new.run  (uses delete_all for the reserved key)","handlingStrategy":"validation","validationCode":"# In cleanup code\nApiKey.visible.each(&:destroy)                       # skips demo key\n# or guard explicitly\nkeys.each { |k| k.destroy unless k.demo_monitoring_key? }","typeGuard":"def destructible_api_key?(key)\n  !key.demo_monitoring_key?\nend","tryCatchPattern":"rescue ActiveRecord::RecordNotDestroyed => e\n  if e.message.include?(\"Cannot destroy demo monitoring API key\")\n    next # reserved key: only Demo::DataCleaner may remove it\n  else\n    raise\n  end\nend","preventionTips":["Exclude the demo key before bulk destroys (visible scope does it for you)","Delete the demo admin user only via demo cleanup, so cascades never hit the guard","In tests, generate random key values, never the reserved constant","Know that both destroy and delete are guarded — there is no in-band bypass"],"tags":["rails","api-keys","protected-record","destroy-guard","demo-data"],"backgroundTag":"protected-record-operation","analyzedSha":"e69894adb92547273377398c15f45c979cd9416a","analyzedAt":"2026-08-21T18:22:41.165Z","schemaVersion":2},"datasetVersion":"2026-08-21T23:17:16.201Z"}