Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Feature: Redact/hide sensitive info on logs page(s). #379

Merged
merged 2 commits into from
Jul 26, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
43 changes: 27 additions & 16 deletions temba/channels/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -93,6 +93,8 @@ class IVRProtocol(Enum):

redact_request_keys = set()
redact_response_keys = set()
force_redact_request_keys = set()
force_redact_response_keys = set()

def is_available_to(self, user):
"""
Expand Down Expand Up @@ -1243,39 +1245,48 @@ def get_request_display(self, user, anon_mask):
Gets the request trace as it should be displayed to the given user
"""
redact_keys = Channel.get_type_from_code(self.channel.channel_type).redact_request_keys
force_redact_keys = Channel.get_type_from_code(self.channel.channel_type).force_redact_request_keys

return self._get_display_value(user, self.request, anon_mask, redact_keys)
return self._get_display_value(user, self.request, anon_mask, redact_keys, force_redact_keys)

def get_response_display(self, user, anon_mask):
"""
Gets the response trace as it should be displayed to the given user
"""
redact_keys = Channel.get_type_from_code(self.channel.channel_type).redact_response_keys
force_redact_keys = Channel.get_type_from_code(self.channel.channel_type).force_redact_response_keys

return self._get_display_value(user, self.response, anon_mask, redact_keys)
return self._get_display_value(user, self.response, anon_mask, redact_keys, force_redact_keys)

def _get_display_value(self, user, original, mask, redact_keys=()):
def _get_display_value(self, user, original, mask, redact_keys=(), force_redact_keys=()):
"""
Get a part of the log which may or may not have to be redacted to hide sensitive information in anon orgs
"""

if not self.channel.org.is_anon or user.has_org_perm(self.channel.org, "contacts.contact_break_anon"):
return original
def basic_reduction():
if not self.channel.org.is_anon or user.has_org_perm(self.channel.org, "contacts.contact_break_anon"):
return original

# if this log doesn't have a msg then we don't know what to redact, so redact completely
if not self.msg_id:
return mask
# if this log doesn't have a msg then we don't know what to redact, so redact completely
if not self.msg_id:
return mask

needle = self.msg.contact_urn.path
needle = self.msg.contact_urn.path

if redact_keys:
redacted = redact.http_trace(original, needle, mask, redact_keys)
else:
redacted = redact.text(original, needle, mask)
if redact_keys:
result = redact.http_trace(original, needle, mask, redact_keys)
else:
result = redact.text(original, needle, mask)

# if nothing was redacted, don't risk returning sensitive information we didn't find
if original == result:
return mask

return result

# if nothing was redacted, don't risk returning sensitive information we didn't find
if original == redacted:
return mask
redacted = basic_reduction()
if force_redact_keys:
redacted = redact.http_trace(redacted, "********", mask, force_redact_keys)

return redacted

Expand Down
3 changes: 3 additions & 0 deletions temba/channels/types/twilio/type.py
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,9 @@ class TwilioType(ChannelType):
"CalledZip",
}

force_redact_request_keys = {"MessagingServiceSid"}
force_redact_response_keys = {"MessagingServiceSid"}

def is_recommended_to(self, user):
org = user.get_org()
countrycode = timezone_to_country_code(org.timezone)
Expand Down
3 changes: 3 additions & 0 deletions temba/channels/types/twilio_messaging_service/type.py
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,9 @@ class TwilioMessagingServiceType(ChannelType):

attachment_support = True

force_redact_request_keys = {"MessagingServiceSid"}
force_redact_response_keys = {"MessagingServiceSid"}

def is_recommended_to(self, user):
org = user.get_org()
countrycode = timezone_to_country_code(org.timezone)
Expand Down
3 changes: 3 additions & 0 deletions temba/channels/types/twilio_whatsapp/type.py
Original file line number Diff line number Diff line change
Expand Up @@ -55,3 +55,6 @@ class TwilioWhatsappType(ChannelType):
"CalledState",
"CalledZip",
}

force_redact_request_keys = {"MessagingServiceSid"}
force_redact_response_keys = {"MessagingServiceSid"}
3 changes: 3 additions & 0 deletions temba/channels/types/twiml_api/type.py
Original file line number Diff line number Diff line change
Expand Up @@ -62,3 +62,6 @@ class TwimlAPIType(ChannelType):
description=_("Incoming messages for this channel will be sent to this endpoint."),
),
)

force_redact_request_keys = {"MessagingServiceSid"}
force_redact_response_keys = {"MessagingServiceSid"}
28 changes: 28 additions & 0 deletions temba/utils/redact.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import json
import urllib.parse
import xml.sax.saxutils
from collections import OrderedDict
from urllib.parse import parse_qs, urlencode

HTTP_BODY_BOUNDARY = "\r\n\r\n"
Expand Down Expand Up @@ -41,12 +42,39 @@ def text(s, needle, mask):
return s


def replace_headers(raw_data, headers_to_replace):
try:
headers = OrderedDict()
raw_headers, raw_body = raw_data.split(HTTP_BODY_BOUNDARY)
split_headers = str(raw_headers).split("\r\n")

# parse headers
for line in split_headers[1:]:
key, value = line.split(": ")
headers[key] = value

# replace needed
for key, value in headers_to_replace.items():
if key in headers:
headers[key] = value

# rebuild http message
first_header = split_headers[0].strip()
raw_headers = "\r\n".join([first_header] + [": ".join((key, value)) for key, value in headers.items()])
raw_message = HTTP_BODY_BOUNDARY.join((raw_headers, raw_body))
return raw_message
except (KeyError, ValueError):
return raw_data


def http_trace(trace, needle, mask, body_keys=()):
"""
Redacts a value from the given HTTP trace by replacing it with a mask. If body_keys is specified then we also try
to parse the body and replace those keyed values with the mask. Bodies are parsed as JSON and URL encoded.
"""

trace = replace_headers(trace, {"Authorization": "*" * 8})

*rest, body = trace.split(HTTP_BODY_BOUNDARY)

if body and body_keys:
Expand Down
Loading