diff --git a/docker-compose.test.yml b/docker-compose.test.yml index 47cfe0c55ce33636ad4ead51cdf6b9617005652b..44b88b2595f8aa9bfc6518fad80b6cac4a90d49f 100644 --- a/docker-compose.test.yml +++ b/docker-compose.test.yml @@ -2,6 +2,7 @@ version: '3.7' services: notifications-routing: + image: notifications-routing container_name: notifications-routing build: context: . diff --git a/notifications_routing/auditing.py b/notifications_routing/auditing.py index ce1121838cc135fd7e42698ef594efe810098794..81ad92b416302bea3ccda1557b487e7a5cbdfda9 100644 --- a/notifications_routing/auditing.py +++ b/notifications_routing/auditing.py @@ -1,4 +1,5 @@ """Auditing Router definition.""" +import copy import json import logging import uuid @@ -8,8 +9,11 @@ from etcd3 import Client from etcd3.errors import ErrInvalidAuthToken from notifications_routing.config import Config +from notifications_routing.utils import AuditingEvents +EXTERNAL_AUDIT_ROUTE_PREFIX = "/external" + client = None if Config.AUDITING: client = Client(host=Config.ETCD_HOST, port=Config.ETCD_PORT) @@ -17,29 +21,53 @@ if Config.AUDITING: client.auth(username=Config.ETCD_USER, password=Config.ETCD_PASSWORD) -def audit_notification(notification_id, value, user_id=None, key=None): +def _put(notification_id, value, user_id, key, prefix=""): + """Create an audit log.""" + client.put( + ( + f"{prefix}/notifications/{notification_id}/{Config.AUDIT_ID}" + f"/{'target_users/' + user_id + '/' if user_id else ''}{key}" + ), + json.dumps({"date": datetime.now().strftime("%d/%m/%Y %H:%M:%S"), **value}, default=str), + ) + + +def _external_values(value): + """Remove and clean IDs to prepare for external auditing exposure.""" + filtered_value = copy.deepcopy(value) + if value["event"] in [ + AuditingEvents.GET_GROUP_USERS, + AuditingEvents.TARGET_USERS, + AuditingEvents.CHANNEL_USERS_FOR_INTERSECTION, + ]: + del filtered_value["users"] + if value["event"] == AuditingEvents.UNSUSCRIBED_USERS: + filtered_value.update({"targets": len(value["targets"])}) + if value["event"] == AuditingEvents.EXPANDED_RECIPIENTS: + del filtered_value["targets"] + return filtered_value + + +def audit_notification(notification_id, value, user_id=None, key=None, external=False): """Put audit notification information into audit DB.""" + + def _audit(): + _put(notification_id, value, user_id, key) + if external: + _put(notification_id, _external_values(value), user_id, key, prefix=EXTERNAL_AUDIT_ROUTE_PREFIX) + if Config.AUDITING is False: logging.info("Audit disabled") return - def put(): - client.put( - ( - f"/notifications/{notification_id}/{Config.AUDIT_ID}" - f"/{'target_users/' + user_id + '/' if user_id else ''}{key}" - ), - json.dumps({"date": datetime.now().strftime("%d/%m/%Y %H:%M:%S"), **value}, default=str), - ) - if not key: key = uuid.uuid4() try: - put() + _audit() except ErrInvalidAuthToken: logging.debug("refresh etcd token") client.auth(username=Config.ETCD_USER, password=Config.ETCD_PASSWORD) - put() + _audit() except Exception: logging.exception("Error auditing to etcd3:") diff --git a/notifications_routing/router.py b/notifications_routing/router.py index d4d28482ef9b01e4f4b4c5e928ac9390b3d196d8..3e1f5b9e28bc8eb47aa7689ef2e6d2d9c5acaba6 100644 --- a/notifications_routing/router.py +++ b/notifications_routing/router.py @@ -15,7 +15,12 @@ from notifications_routing.preferences import ( apply_user_preferences, get_delivery_methods_and_targets, ) -from notifications_routing.utils import InputMessageKeys, OutputMessageKeys, convert_timestamp_to_local_timezone +from notifications_routing.utils import ( + AuditingEvents, + InputMessageKeys, + OutputMessageKeys, + convert_timestamp_to_local_timezone, +) # ActiveMQ @@ -89,7 +94,9 @@ class Router(megabus.Listener): for group_id in groups: group_users = self.data_source.get_group_users(group_id) - audit_notification(notification_id, {"event": "get_group_users", "group": group_id, "users": group_users}) + audit_notification( + notification_id, {"event": AuditingEvents.GET_GROUP_USERS, "group": group_id, "users": group_users} + ) logging.debug("channel %s groups users %s", channel_id, group_users) for user in group_users: @@ -117,7 +124,18 @@ class Router(megabus.Listener): unsubscribed_users = self.data_source.get_channel_unsubscribed_users(channel_id) logging.debug("channel %s unsubscribed users %s", channel_id, unsubscribed_users) - audit_notification(notification_id, {"event": "Unsubscribed users", "targets": unsubscribed_users}) + audit_notification( + notification_id, {"event": AuditingEvents.UNSUSCRIBED_USERS, "targets": unsubscribed_users}, external=True + ) + audit_notification( + notification_id, + { + "event": AuditingEvents.CHANNEL_SNAPSHOT, + "target users": users + unsubscribed_users, + "target groups": groups, + }, + external=True, + ) if groups: self.add_users_from_groups(notification_id, channel_id, users, groups, unsubscribed_users) @@ -134,12 +152,23 @@ class Router(megabus.Listener): target_groups = self.data_source.get_target_groups(notification_id) logging.debug("channel %s targeted groups %s", channel_id, target_groups) + audit_notification( + notification_id, + { + "event": AuditingEvents.NOTIFICATION_TARGETS, + "targets users": target_users, + "target_groups": target_groups, + }, + external=True, + ) if not (target_groups or target_users): return [] unsubscribed_users = self.data_source.get_channel_unsubscribed_users(channel_id) logging.debug("channel %s unsubscribed users %s", channel_id, unsubscribed_users) - audit_notification(notification_id, {"event": "Unsubscribed users", "targets": unsubscribed_users}) + audit_notification( + notification_id, {"event": AuditingEvents.UNSUSCRIBED_USERS, "targets": unsubscribed_users}, external=True + ) subscribed_target_users = [user for user in target_users if user[DataSource.USERNAME] not in unsubscribed_users] logging.debug("channel %s subscribed targeted users %s", channel_id, target_users) @@ -159,7 +188,11 @@ class Router(megabus.Listener): :raises: NotFoundDataSourceError, MultipleResultsFoundError """ if OutputMessageKeys.PRIVATE in message and message[OutputMessageKeys.PRIVATE]: - audit_notification(message[OutputMessageKeys.ID], {"event": "Start processing targeted notification"}) + audit_notification( + message[OutputMessageKeys.ID], + {"event": AuditingEvents.START_PROCESSING_TARGETED_NOTIFICATION}, + external=True, + ) logging.debug("Processing direct notification %s", message[OutputMessageKeys.ID]) target_users = self.get_target_users( message[OutputMessageKeys.ID], @@ -177,11 +210,17 @@ class Router(megabus.Listener): audit_notification( message[OutputMessageKeys.ID], - {"event": "Target users", "total": len(target_users), "users": target_users}, + {"event": AuditingEvents.TARGET_USERS, "total": len(target_users), "users": target_users}, + external=True, ) audit_notification( message[OutputMessageKeys.ID], - {"event": "Channel users for intersection", "total": len(channel_users), "users": channel_users}, + { + "event": AuditingEvents.CHANNEL_USERS_FOR_INTERSECTION, + "total": len(channel_users), + "users": channel_users, + }, + external=True, ) if not channel_users: logging.debug("no channel_users to intersect for channel %s", message[OutputMessageKeys.CHANNEL_ID]) @@ -190,7 +229,7 @@ class Router(megabus.Listener): return target_users else: - audit_notification(message[OutputMessageKeys.ID], {"event": "Start processing"}) + audit_notification(message[OutputMessageKeys.ID], {"event": AuditingEvents.START_PROCESSING}, external=True) logging.debug("Processing general notification %s", message[OutputMessageKeys.ID]) target_users = self.get_channel_users(message[OutputMessageKeys.ID], message[OutputMessageKeys.CHANNEL_ID]) if not target_users: @@ -204,7 +243,8 @@ class Router(megabus.Listener): target_users = self.process_users(message) audit_notification( message[OutputMessageKeys.ID], - {"event": "Expanded recipients", "total": len(target_users), "targets": target_users}, + {"event": AuditingEvents.EXPANDED_RECIPIENTS, "total": len(target_users), "targets": target_users}, + external=True, ) if not target_users: diff --git a/notifications_routing/utils.py b/notifications_routing/utils.py index 0bf9cf81e1a4a3ec7fdb79ab96b7f358b50c3d4c..1c10bde9a99e06cea93149d5b50fe0f8d3a7ee14 100644 --- a/notifications_routing/utils.py +++ b/notifications_routing/utils.py @@ -58,6 +58,20 @@ class FeedFrequency(StrEnum): MONTHLY = "MONTHLY" +class AuditingEvents: + """Audit event types.""" + + GET_GROUP_USERS = "Get group users" + UNSUSCRIBED_USERS = "Unsubscribed users" + START_PROCESSING_TARGETED_NOTIFICATION = "Start processing targeted notification" + START_PROCESSING = "Start processing" + TARGET_USERS = "Target users" + CHANNEL_USERS_FOR_INTERSECTION = "Computed users for intersection" + EXPANDED_RECIPIENTS = "Expanded recipients" + CHANNEL_SNAPSHOT = "Channel snapshot" + NOTIFICATION_TARGETS = "Notification targets" + + def is_time_between(time, start_range, end_range): """Check if time is between a range.