diff --git a/DEPLOYMENT.md b/DEPLOYMENT.md index c50e8114..b7e7b044 100644 --- a/DEPLOYMENT.md +++ b/DEPLOYMENT.md @@ -48,6 +48,8 @@ with Quadlet (systemd-managed containers). - [API observability](#api-observability) - [Logs](#logs) - [Redis counters](#redis-counters) +- [Account observability](#account-observability) + - [Finding duplicate accounts by IP](#finding-duplicate-accounts-by-ip) - [Useful commands](#useful-commands) - [File locations on the droplet](#file-locations-on-the-droplet) @@ -1023,6 +1025,38 @@ flipped back to `is_active=True` in Django admin after review. --- +## Account observability + +Signup and account-activation views log the username and client IP (see +`_client_ip` in `users/views.py`), in the same `(user=..., ip=...)` format used +by the `authenticated via` API auth logs (see `api/authentication.py`). Because +the format is shared, all three log lines can be mined together to spot one IP +behind multiple usernames. + +### Finding duplicate accounts by IP + +```bash +# IPs that signed up for more than one account +journalctl _SYSTEMD_USER_UNIT=metron-web.service | grep "signed up for an account" \ + | grep -oP 'ip=\K\S+' | sort | uniq -c | sort -rn | awk '$1 > 1' + +# IPs seen under more than one username, across signups, activations, and +# authenticated API requests +journalctl _SYSTEMD_USER_UNIT=metron-web.service \ + | grep -E "signed up for an account|activated their account|authenticated via" \ + | grep -oP '\(user=[^,]+, ip=[^)]+\)' \ + | sed -E 's/\(user=([^,]+), ip=([^)]+)\)/\2 \1/' \ + | sort -u \ + | awk '{ users[$1] = users[$1]" "$2 } END { for (ip in users) if (split(users[ip], a, " ") > 1) print ip":"users[ip] }' + +# All signup/activation/API-auth log lines for a specific IP +journalctl _SYSTEMD_USER_UNIT=metron-web.service \ + | grep -E "signed up for an account|activated their account|authenticated via" \ + | grep "ip=" +``` + +--- + ## Useful commands ```bash diff --git a/users/views.py b/users/views.py index 7b285d71..64966274 100644 --- a/users/views.py +++ b/users/views.py @@ -116,7 +116,13 @@ def activate(request, uidb64, token): login(request, user) # Send pushover notification tha user activated account send_pushover(f"{user} activated their account on Metron.") - logger.info("%s activated their account on Metron", user) + ip = _client_ip(request) + logger.info( + "User activated their account on Metron (user=%s, ip=%s)", + user.username, + ip, + extra={"username": user.username, "ip": ip}, + ) # Add a message asking the user to star the repository. msg = mark_safe( "If you are planning on adding new information to the database, please refer to the " @@ -173,7 +179,13 @@ def signup(request): # sourcery skip: extract-method email.send() # Let's send a pushover notice that a user requested an account. send_pushover(f"{user} signed up for an account on Metron.") - logger.info("%s signed up for an account on Metron", user) + ip = _client_ip(request) + logger.info( + "User signed up for an account on Metron (user=%s, ip=%s)", + user.username, + ip, + extra={"username": user.username, "ip": ip}, + ) return redirect("account_activation_sent") else: