Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
9c518ea443 | ||
|
|
546e953513 | ||
|
|
9595e5578b | ||
|
|
c9bfc5c6a8 | ||
|
|
f5d80f2064 | ||
|
|
d5f46f922d | ||
|
|
4339e0ee0a | ||
|
|
e76650d6d0 | ||
|
|
3b7b94f8d1 |
+1
-1
@@ -1 +1 @@
|
||||
4.14.0
|
||||
4.14.1
|
||||
|
||||
@@ -30,9 +30,8 @@ class Public::Api::V1::Inboxes::ContactsController < Public::Api::V1::InboxesCon
|
||||
def process_hmac
|
||||
return if params[:identifier_hash].blank? && !@inbox_channel.hmac_mandatory
|
||||
raise StandardError, 'HMAC failed: Invalid Identifier Hash Provided' unless valid_hmac?
|
||||
raise StandardError, 'HMAC failed: Identifier does not match contact' if hmac_identifier_conflict?
|
||||
|
||||
@contact_inbox.update(hmac_verified: true) if @contact_inbox.present? && hmac_identifier_matches?
|
||||
@contact_inbox.update(hmac_verified: true) if @contact_inbox.present?
|
||||
end
|
||||
|
||||
def valid_hmac?
|
||||
@@ -43,24 +42,6 @@ class Public::Api::V1::Inboxes::ContactsController < Public::Api::V1::InboxesCon
|
||||
)
|
||||
end
|
||||
|
||||
# Rejects the request when the selected contact is already bound to a different identifier,
|
||||
# so a valid (identifier, identifier_hash) pair cannot be replayed against another contact.
|
||||
def hmac_identifier_conflict?
|
||||
return false if @contact_inbox.blank?
|
||||
|
||||
contact_identifier = @contact_inbox.contact.identifier.to_s
|
||||
contact_identifier.present? &&
|
||||
!ActiveSupport::SecurityUtils.secure_compare(contact_identifier, params[:identifier].to_s)
|
||||
end
|
||||
|
||||
# Only grants verified trust when the proven identifier matches the contact's own identifier.
|
||||
# A blank contact identifier never grants trust, preventing replay onto anonymous contacts.
|
||||
def hmac_identifier_matches?
|
||||
contact_identifier = @contact_inbox.contact.identifier.to_s
|
||||
contact_identifier.present? &&
|
||||
ActiveSupport::SecurityUtils.secure_compare(contact_identifier, params[:identifier].to_s)
|
||||
end
|
||||
|
||||
def permitted_params
|
||||
params.permit(:identifier, :identifier_hash, :email, :name, :avatar_url, :phone_number, custom_attributes: {})
|
||||
end
|
||||
|
||||
@@ -554,11 +554,10 @@ provideMessageContext({
|
||||
<Avatar v-bind="avatarInfo" :size="24" />
|
||||
</div>
|
||||
<div
|
||||
class="[grid-area:bubble] flex"
|
||||
class="[grid-area:bubble] flex min-w-0"
|
||||
:class="{
|
||||
'ltr:ml-8 rtl:mr-8 justify-end': orientation === ORIENTATION.RIGHT,
|
||||
'ltr:mr-8 rtl:ml-8': orientation === ORIENTATION.LEFT,
|
||||
'min-w-0': variant === MESSAGE_VARIANTS.EMAIL,
|
||||
}"
|
||||
@contextmenu="openContextMenu($event)"
|
||||
>
|
||||
|
||||
@@ -95,7 +95,7 @@ const replyToPreview = computed(() => {
|
||||
|
||||
<template>
|
||||
<div
|
||||
class="text-sm"
|
||||
class="text-sm min-w-0"
|
||||
:class="[
|
||||
messageClass,
|
||||
{
|
||||
|
||||
+1
-1
@@ -1,5 +1,5 @@
|
||||
shared: &shared
|
||||
version: '4.14.0'
|
||||
version: '4.14.1'
|
||||
|
||||
development:
|
||||
<<: *shared
|
||||
|
||||
@@ -265,6 +265,12 @@
|
||||
display_title: 'Analytics Token'
|
||||
description: 'The Amplitude analytics API key for Chatwoot cloud'
|
||||
type: secret
|
||||
- name: CHATWOOT_CLOUD_SIGNALS_API_TOKEN
|
||||
value:
|
||||
display_title: 'Signals API Token'
|
||||
description: 'Internal API token for Signals to sync Chatwoot cloud attribution data'
|
||||
locked: true
|
||||
type: secret
|
||||
- name: CLEARBIT_API_KEY
|
||||
value:
|
||||
display_title: 'Clearbit API Key'
|
||||
|
||||
@@ -555,6 +555,11 @@ Rails.application.routes.draw do
|
||||
end
|
||||
resources :email_channel_migrations, only: [:create]
|
||||
end
|
||||
if ChatwootApp.chatwoot_cloud?
|
||||
namespace :internal do
|
||||
resources :accounts, only: [:index]
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
@@ -34,10 +34,10 @@ module Enterprise::SuperAdmin::AppConfigsController
|
||||
end
|
||||
|
||||
def internal_config_options
|
||||
%w[CHATWOOT_INBOX_TOKEN CHATWOOT_INBOX_HMAC_KEY CLOUD_ANALYTICS_TOKEN CLEARBIT_API_KEY CONTEXT_DEV_API_KEY DASHBOARD_SCRIPTS
|
||||
INACTIVE_WHATSAPP_NUMBERS SKIP_INCOMING_BCC_PROCESSING CAPTAIN_CLOUD_PLAN_LIMITS ACCOUNT_SECURITY_NOTIFICATION_WEBHOOK_URL
|
||||
CHATWOOT_INSTANCE_ADMIN_EMAIL OG_IMAGE_CDN_URL OG_IMAGE_CLIENT_REF CLOUDFLARE_API_KEY CLOUDFLARE_ZONE_ID BLOCKED_EMAIL_DOMAINS
|
||||
OTEL_PROVIDER LANGFUSE_PUBLIC_KEY LANGFUSE_SECRET_KEY LANGFUSE_BASE_URL]
|
||||
%w[CHATWOOT_INBOX_TOKEN CHATWOOT_INBOX_HMAC_KEY CLOUD_ANALYTICS_TOKEN CHATWOOT_CLOUD_SIGNALS_API_TOKEN CLEARBIT_API_KEY
|
||||
CONTEXT_DEV_API_KEY DASHBOARD_SCRIPTS INACTIVE_WHATSAPP_NUMBERS SKIP_INCOMING_BCC_PROCESSING CAPTAIN_CLOUD_PLAN_LIMITS
|
||||
ACCOUNT_SECURITY_NOTIFICATION_WEBHOOK_URL CHATWOOT_INSTANCE_ADMIN_EMAIL OG_IMAGE_CDN_URL OG_IMAGE_CLIENT_REF CLOUDFLARE_API_KEY
|
||||
CLOUDFLARE_ZONE_ID BLOCKED_EMAIL_DOMAINS OTEL_PROVIDER LANGFUSE_PUBLIC_KEY LANGFUSE_SECRET_KEY LANGFUSE_BASE_URL]
|
||||
end
|
||||
|
||||
def captain_config_options
|
||||
|
||||
@@ -0,0 +1,38 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
class Platform::Api::V1::Internal::AccountsController < ActionController::API
|
||||
DEFAULT_LIMIT = 100
|
||||
MAX_LIMIT = 1000
|
||||
|
||||
before_action :ensure_chatwoot_cloud
|
||||
before_action :authenticate_internal_token!
|
||||
|
||||
def index
|
||||
@accounts = filtered_accounts.limit(limit)
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def ensure_chatwoot_cloud
|
||||
render json: { error: 'Not found' }, status: :not_found unless ChatwootApp.chatwoot_cloud?
|
||||
end
|
||||
|
||||
def authenticate_internal_token!
|
||||
token = request.headers[:api_access_token] || request.headers[:HTTP_API_ACCESS_TOKEN]
|
||||
config_token = GlobalConfigService.load('CHATWOOT_CLOUD_SIGNALS_API_TOKEN', nil)
|
||||
return if token.present? && config_token.present? && ActiveSupport::SecurityUtils.secure_compare(token, config_token)
|
||||
|
||||
render json: { error: 'Invalid access_token' }, status: :unauthorized
|
||||
end
|
||||
|
||||
def filtered_accounts
|
||||
scope = Account.order(:created_at, :id)
|
||||
scope = scope.where('created_at >= ?', Time.zone.at(params[:since].to_i)) if params[:since].present?
|
||||
scope = scope.where('created_at <= ?', Time.zone.at(params[:until].to_i)) if params[:until].present?
|
||||
scope
|
||||
end
|
||||
|
||||
def limit
|
||||
params.fetch(:limit, DEFAULT_LIMIT).to_i.clamp(1, MAX_LIMIT)
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,14 @@
|
||||
json.array! @accounts do |account|
|
||||
json.id account.id
|
||||
json.name account.name
|
||||
json.created_at account.created_at
|
||||
json.updated_at account.updated_at
|
||||
json.status account.status
|
||||
json.plan_name account.custom_attributes['plan_name']
|
||||
json.stripe_customer_id account.custom_attributes['stripe_customer_id']
|
||||
json.stripe_price_id account.custom_attributes['stripe_price_id']
|
||||
json.stripe_product_id account.custom_attributes['stripe_product_id']
|
||||
json.subscription_status account.custom_attributes['subscription_status']
|
||||
json.limits account.limits
|
||||
json.marketing_attribution account.internal_attributes['marketing_attribution']
|
||||
end
|
||||
+1
-1
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@chatwoot/chatwoot",
|
||||
"version": "4.14.0",
|
||||
"version": "4.14.1",
|
||||
"license": "MIT",
|
||||
"scripts": {
|
||||
"eslint": "eslint app/**/*.{js,vue}",
|
||||
|
||||
@@ -48,54 +48,4 @@ RSpec.describe 'Public Inbox Contacts API', type: :request do
|
||||
expect(data['name']).to eq 'John Smith'
|
||||
end
|
||||
end
|
||||
|
||||
describe 'HMAC identifier binding' do
|
||||
let!(:owner_contact) { create(:contact, account: api_channel.account, identifier: 'owner-identifier') }
|
||||
let!(:owner_inbox) { create(:contact_inbox, contact: owner_contact, inbox: api_channel.inbox) }
|
||||
let!(:victim_contact) { create(:contact, account: api_channel.account, identifier: 'victim-identifier', name: 'Victim') }
|
||||
let!(:victim_inbox) { create(:contact_inbox, contact: victim_contact, inbox: api_channel.inbox) }
|
||||
let(:valid_owner_hash) { OpenSSL::HMAC.hexdigest('sha256', api_channel.hmac_token, 'owner-identifier') }
|
||||
|
||||
it 'does not promote the victim contact inbox to hmac_verified when a valid pair is replayed against the victim source_id' do
|
||||
begin
|
||||
get "/public/api/v1/inboxes/#{api_channel.identifier}/contacts/#{victim_inbox.source_id}",
|
||||
params: { identifier: 'owner-identifier', identifier_hash: valid_owner_hash }
|
||||
rescue StandardError
|
||||
# the fix fails closed by raising; the security invariant is asserted below
|
||||
end
|
||||
|
||||
expect(victim_inbox.reload.hmac_verified).to be(false)
|
||||
end
|
||||
|
||||
it 'does not mutate the victim contact when a valid pair is replayed against the victim source_id' do
|
||||
begin
|
||||
patch "/public/api/v1/inboxes/#{api_channel.identifier}/contacts/#{victim_inbox.source_id}",
|
||||
params: { identifier: 'owner-identifier', identifier_hash: valid_owner_hash, name: 'Hacked' }
|
||||
rescue StandardError
|
||||
# the fix fails closed by raising; the security invariant is asserted below
|
||||
end
|
||||
|
||||
expect(victim_contact.reload.name).to eq('Victim')
|
||||
end
|
||||
|
||||
it 'still verifies the contact inbox when the owner presents a matching identifier pair' do
|
||||
get "/public/api/v1/inboxes/#{api_channel.identifier}/contacts/#{owner_inbox.source_id}",
|
||||
params: { identifier: 'owner-identifier', identifier_hash: valid_owner_hash }
|
||||
|
||||
expect(response).to have_http_status(:success)
|
||||
expect(owner_inbox.reload.hmac_verified).to be(true)
|
||||
end
|
||||
|
||||
it 'verifies an API contact created without an identifier when a valid pair is supplied later' do
|
||||
post "/public/api/v1/inboxes/#{api_channel.identifier}/contacts"
|
||||
|
||||
anonymous_source_id = response.parsed_body['source_id']
|
||||
anonymous_inbox = api_channel.inbox.contact_inboxes.find_by!(source_id: anonymous_source_id)
|
||||
|
||||
get "/public/api/v1/inboxes/#{api_channel.identifier}/contacts/#{anonymous_source_id}",
|
||||
params: { identifier: 'owner-identifier', identifier_hash: valid_owner_hash }
|
||||
|
||||
expect(anonymous_inbox.reload.hmac_verified).to be(true)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
Reference in New Issue
Block a user