mirror of
https://github.com/zammad/zammad
synced 2026-05-24 09:48:36 +00:00
155 lines
7 KiB
Ruby
155 lines
7 KiB
Ruby
# Copyright (C) 2012-2026 Zammad Foundation, https://zammad-foundation.org/
|
|
|
|
require 'rails_helper'
|
|
|
|
RSpec.describe HttpLog, :aggregate_failures do
|
|
subject(:http_log) { build(:http_log) }
|
|
|
|
describe 'callbacks' do
|
|
# See https://github.com/zammad/zammad/issues/2100
|
|
it 'converts request/response message data to UTF-8 before saving' do
|
|
http_log.request[:content] = 'foo'.dup.force_encoding('ascii-8bit')
|
|
http_log.response[:content] = 'bar'.dup.force_encoding('ascii-8bit')
|
|
|
|
expect { http_log.save }
|
|
.to change { http_log.request[:content].encoding.name }.from('ASCII-8BIT').to('UTF-8')
|
|
.and change { http_log.response[:content].encoding.name }.from('ASCII-8BIT').to('UTF-8')
|
|
end
|
|
|
|
context 'when sensitive data is present' do
|
|
subject(:http_log) { create(:http_log, request: { headers: "Authorization: Bearer supersecrettoken\nCookie: session_id=12345; user_id=67890", url: 'https://example.com/api?access_token=query_secret_token&other=param' }, response: { body: 'access_token="anothersecret" api_key: "key-value" secret = "my_secret"' }) }
|
|
|
|
it 'masks sensitive data in request/response before saving', :aggregate_failures do
|
|
expect(http_log.request['headers']).to eq("Authorization: Bearer [FILTERED]\nCookie: session_id=[FILTERED]; user_id=[FILTERED]")
|
|
expect(http_log.request['url']).to eq('https://example.com/api?access_token=[FILTERED]&other=param')
|
|
expect(http_log.response['body']).to eq('access_token="[FILTERED]" api_key: "[FILTERED]" secret = "[FILTERED]"')
|
|
end
|
|
end
|
|
|
|
context 'when long data is present' do
|
|
subject(:http_log) do
|
|
create(:http_log, request: { content: "{\"images\":[\"iVBORw0KGgoAAAANSUhEUgAABEkAAAECCAYAAAACQolFAAABY2lDQ1BrQ0dDb2xvclNwYWNlRGlzcGxheVAzAAAok\nX2QsUvDUBDGv1aloHUQHRwcMolDlJIKuji0FURxCFXB6pS+pqmQxkeSIgU3/4GC/4EKzm4Whzo6OAiik+jm5KTgouV5L4mkInqP435877vjOCA5bn\nBu9wOoO75bXMorm6UtJfWMBL0gDObxnK6vSv6uP+P9PvTeTstZv///jcGK6TGqn5QZxl0fSKjE+p7PJe8Tj7m0FHFLshXyieRyyOeBZ71YIL4mVlj\nNqBC/EKvlHt3q4brdYNEOcvu06WysyTmUE1jEDjxw2DDQhAId2T/8s4G/gF1yN\nFSn4UafOrJkSI\"]}" }, response: { body: "data:image/png;base64,iVBORw0KGgoAAAAAACWZVhJZk1NACoAAAAIAAUBEgADAAAAAQABAAABGgAFAAAAAQAAAEoBGwAFAAAAAQAAAFIBKAADAAAAAQACAACHaQAEAAAAAQ
|
|
AAAFoAAAAAAAAAkAAAAAEAAACQAAAAAQADk=" })
|
|
end
|
|
|
|
it 'truncated long data in request/response before saving', :aggregate_failures do
|
|
expect(http_log.request['content']).to eq('{"images":["iVBORw0KGgoAAAANSUhEUgAABEkAA...[TRUNCATED]"]}')
|
|
expect(http_log.response['body']).to eq('data:image/png;base64,iVBORw0...[TRUNCATED]')
|
|
end
|
|
end
|
|
end
|
|
|
|
describe '.mask_sensitive_data' do
|
|
let(:input) { 'Authorization: Basic dXNlcjpwYXNzd29yZA==' }
|
|
|
|
context 'when the input includes authorization basic part' do
|
|
it 'masks Basic authorization headers' do
|
|
expect(described_class.mask_sensitive_data(input)).to eq('Authorization: Basic [FILTERED]')
|
|
end
|
|
end
|
|
|
|
context 'when input has no sensitive parts' do
|
|
it 'returns the input unchanged' do
|
|
plain = 'no secrets here'
|
|
expect(described_class.mask_sensitive_data(plain)).to eq('no secrets here')
|
|
end
|
|
end
|
|
|
|
context 'when the input includes authorization bearer part' do
|
|
it 'masks Bearer authorization headers' do
|
|
bearer = 'Authorization: Bearer supersecrettoken'
|
|
expect(described_class.mask_sensitive_data(bearer)).to eq('Authorization: Bearer [FILTERED]')
|
|
end
|
|
end
|
|
|
|
context 'when the input includes sensitive query parameters' do
|
|
it 'masks only the sensitive query parameter values' do
|
|
url = 'https://example.com/api?api_key=abc123&other=param'
|
|
expect(described_class.mask_sensitive_data(url)).to eq('https://example.com/api?api_key=[FILTERED]&other=param')
|
|
end
|
|
end
|
|
|
|
context 'when the input includes cookies' do
|
|
it 'masks cookie values but keeps names' do
|
|
cookies = 'Cookie: session_id=12345; user_id=67890'
|
|
expect(described_class.mask_sensitive_data(cookies)).to eq('Cookie: session_id=[FILTERED]; user_id=[FILTERED]')
|
|
end
|
|
end
|
|
|
|
context 'when the input includes inline tokens or secrets' do
|
|
it 'masks inline token-like values' do
|
|
text = 'api_key: 12345 secret="s3cr3t" foo=bar'
|
|
expect(described_class.mask_sensitive_data(text)).to eq('api_key: [FILTERED] secret="[FILTERED]" foo=bar')
|
|
end
|
|
end
|
|
end
|
|
|
|
describe 'validations' do
|
|
it 'is valid with valid attributes' do
|
|
expect(http_log).to be_valid
|
|
end
|
|
|
|
it 'is invalid with unknown facility' do
|
|
http_log.facility = 'unknown'
|
|
expect(http_log).not_to be_valid
|
|
expect(http_log.errors[:facility]).to include('is not included in the list')
|
|
end
|
|
end
|
|
|
|
describe '.facility_to_permission' do
|
|
it 'returns correct permission for known facility' do
|
|
expect(described_class.facility_to_permission('GitHub')).to eq('admin.integration')
|
|
expect(described_class.facility_to_permission('webhook')).to eq('admin.webhook')
|
|
expect(described_class.facility_to_permission('cti')).to eq('admin.integration')
|
|
expect(described_class.facility_to_permission('AI::Provider')).to eq('admin.ai_provider')
|
|
expect(described_class.facility_to_permission('WhatsApp::Business')).to eq('admin.channel_whatsapp')
|
|
end
|
|
|
|
it 'returns admin.* for blank facility' do
|
|
expect(described_class.facility_to_permission(nil)).to eq('admin.*')
|
|
expect(described_class.facility_to_permission('')).to eq('admin.*')
|
|
end
|
|
|
|
it 'returns nil for unknown facility' do
|
|
expect(described_class.facility_to_permission('unknown')).to be_nil
|
|
end
|
|
end
|
|
|
|
describe '.facilities_by_permission' do
|
|
it 'returns a hash grouped by permissions with facilities' do
|
|
expect(described_class.facilities_by_permission).to include(
|
|
'admin.ai_provider' => include('AI::Provider'),
|
|
'admin.integration' => include('GitHub'),
|
|
'admin.security' => include('SAML'),
|
|
'admin.webhook' => include('webhook'),
|
|
'admin.channel_whatsapp' => include('WhatsApp::Business')
|
|
)
|
|
end
|
|
end
|
|
|
|
describe '.truncate_long_data' do
|
|
let(:input) { 'R0lGODlhAQABAAAAACH5BAEKAAEALAAAAAABAAEAAAICTAEAOw==' }
|
|
|
|
context 'when the input includes long base64 data' do
|
|
it 'truncates long data' do
|
|
expect(described_class.truncate_long_data(input)).to eq('R0lGODlhAQABAAAAACH5BAEKAAEAL...[TRUNCATED]')
|
|
end
|
|
end
|
|
|
|
context 'when the input includes base64 data URL prefix' do
|
|
let(:input) { 'data:image/gif;base64,R0lGfODlhAQABAAAAACH5BAEKAAEALAAAAAABAAEAAAICTAEAOw==' }
|
|
|
|
it 'truncates long data but preserves the prefix' do
|
|
expect(described_class.truncate_long_data(input)).to eq('data:image/gif;base64,R0lGfOD...[TRUNCATED]')
|
|
end
|
|
end
|
|
|
|
context 'when input is not longer than 32 chars' do
|
|
let(:input) { 'R0lGODlhAQABAAAAACH5BAEKAAEALAA=' }
|
|
|
|
it 'returns the input unchanged' do
|
|
expect(described_class.truncate_long_data(input)).to eq(input)
|
|
end
|
|
end
|
|
end
|
|
end
|