Skip to content

Commit

Permalink
Merge pull request #379 from greatnonprofits-nfp/feature/hide-sens-logs
Browse files Browse the repository at this point in the history
Feature: Redact/hide sensitive info on logs page(s).
  • Loading branch information
teehamaral authored Jul 26, 2023
2 parents 4c625ec + acee342 commit 1625c96
Show file tree
Hide file tree
Showing 6 changed files with 67 additions and 16 deletions.
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

0 comments on commit 1625c96

Please sign in to comment.