mirror of
https://github.com/lingble/chatwoot.git
synced 2025-10-29 02:02:27 +00:00
## Changelog
- Added conditional Active Record encryption to every external
credential we store (SMTP/IMAP passwords, Twilio tokens,
Slack/OpenAI hook tokens, Facebook/Instagram tokens, LINE/Telegram keys,
Twitter secrets) so new writes are encrypted
whenever Chatwoot.encryption_configured? is true; legacy installs still
receive plaintext until their secrets are
updated.
- Tuned encryption settings in config/application.rb to allow legacy
reads (support_unencrypted_data) and to extend
deterministic queries so lookups continue to match plaintext rows during
the rollout; added TODOs to retire the
fallback once encryption becomes mandatory.
- Introduced an MFA-pipeline test suite
(spec/models/external_credentials_encryption_spec.rb) plus shared
examples to
verify each attribute encrypts at rest and that plaintext records
re-encrypt on update, with a dedicated Telegram case.
The existing MFA GitHub workflow now runs these tests using the
preconfigured encryption keys.
fixes:
https://linear.app/chatwoot/issue/CW-5453/encrypt-sensitive-credentials-stored-in-plain-text-in-database
## Testing Instructions
1. Instance without encryption keys
- Unset ACTIVE_RECORD_ENCRYPTION_* vars (or run in an environment where
they’re absent).
- Create at least one credentialed channel (e.g., Email SMTP).
- Confirm workflows still function (send/receive mail or a similar
sanity check).
- In the DB you should still see plaintext values—this confirms the
guard prevents encryption when keys are missing.
2. Instance with encryption keys
- Configure the three encryption env vars and restart.
- Pick a couple of representative integrations (e.g., Email SMTP +
Twilio SMS).
- Legacy channel check:
- Use existing records created before enabling keys. Trigger their
workflow (send an email / SMS, or hit the
webhook) to ensure they still authenticate.
- Inspect the raw column—value remains plaintext until changed.
- Update legacy channel:
- Edit one legacy channel’s credential (e.g., change SMTP password).
- Verify the operation still works and the stored value is now encrypted
(raw column differs, accessor returns
original).
- New channel creation:
- Create a new channel of the same type; confirm functionality and that
the stored credential is encrypted from
the start.
---------
Co-authored-by: Muhsin Keloth <muhsinkeramam@gmail.com>
114 lines
3.1 KiB
Ruby
114 lines
3.1 KiB
Ruby
# == Schema Information
|
|
#
|
|
# Table name: integrations_hooks
|
|
#
|
|
# id :bigint not null, primary key
|
|
# access_token :string
|
|
# hook_type :integer default("account")
|
|
# settings :jsonb
|
|
# status :integer default("enabled")
|
|
# created_at :datetime not null
|
|
# updated_at :datetime not null
|
|
# account_id :integer
|
|
# app_id :string
|
|
# inbox_id :integer
|
|
# reference_id :string
|
|
#
|
|
class Integrations::Hook < ApplicationRecord
|
|
include Reauthorizable
|
|
|
|
attr_readonly :app_id, :account_id, :inbox_id, :hook_type
|
|
before_validation :ensure_hook_type
|
|
after_create :trigger_setup_if_crm
|
|
|
|
# TODO: Remove guard once encryption keys become mandatory (target 3-4 releases out).
|
|
encrypts :access_token, deterministic: true if Chatwoot.encryption_configured?
|
|
|
|
validates :account_id, presence: true
|
|
validates :app_id, presence: true
|
|
validates :inbox_id, presence: true, if: -> { hook_type == 'inbox' }
|
|
validate :validate_settings_json_schema
|
|
validate :ensure_feature_enabled
|
|
validates :app_id, uniqueness: { scope: [:account_id], unless: -> { app.present? && app.params[:allow_multiple_hooks].present? } }
|
|
|
|
# TODO: This seems to be only used for slack at the moment
|
|
# We can add a validator when storing the integration settings and toggle this in future
|
|
enum status: { disabled: 0, enabled: 1 }
|
|
|
|
belongs_to :account
|
|
belongs_to :inbox, optional: true
|
|
has_secure_token :access_token
|
|
|
|
enum hook_type: { account: 0, inbox: 1 }
|
|
|
|
scope :account_hooks, -> { where(hook_type: 'account') }
|
|
scope :inbox_hooks, -> { where(hook_type: 'inbox') }
|
|
|
|
def app
|
|
@app ||= Integrations::App.find(id: app_id)
|
|
end
|
|
|
|
def slack?
|
|
app_id == 'slack'
|
|
end
|
|
|
|
def dialogflow?
|
|
app_id == 'dialogflow'
|
|
end
|
|
|
|
def notion?
|
|
app_id == 'notion'
|
|
end
|
|
|
|
def disable
|
|
update(status: 'disabled')
|
|
end
|
|
|
|
def process_event(event)
|
|
case app_id
|
|
when 'openai'
|
|
Integrations::Openai::ProcessorService.new(hook: self, event: event).perform if app_id == 'openai'
|
|
else
|
|
{ error: 'No processor found' }
|
|
end
|
|
end
|
|
|
|
def feature_allowed?
|
|
return true if app.blank?
|
|
|
|
flag = app.params[:feature_flag]
|
|
return true unless flag
|
|
|
|
account.feature_enabled?(flag)
|
|
end
|
|
|
|
private
|
|
|
|
def ensure_feature_enabled
|
|
errors.add(:feature_flag, 'Feature not enabled') unless feature_allowed?
|
|
end
|
|
|
|
def ensure_hook_type
|
|
self.hook_type = app.params[:hook_type] if app.present?
|
|
end
|
|
|
|
def validate_settings_json_schema
|
|
return if app.blank? || app.params[:settings_json_schema].blank?
|
|
|
|
errors.add(:settings, ': Invalid settings data') unless JSONSchemer.schema(app.params[:settings_json_schema]).valid?(settings)
|
|
end
|
|
|
|
def trigger_setup_if_crm
|
|
# we need setup services to create data prerequisite to functioning of the integration
|
|
# in case of Leadsquared, we need to create a custom activity type for capturing conversations and transcripts
|
|
# https://apidocs.leadsquared.com/create-new-activity-type-api/
|
|
return unless crm_integration?
|
|
|
|
::Crm::SetupJob.perform_later(id)
|
|
end
|
|
|
|
def crm_integration?
|
|
%w[leadsquared].include?(app_id)
|
|
end
|
|
end
|