From 60a070731ec4ed63da88eb7175f55bef4963aa61 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Adri=C3=A0=20Casaj=C3=BAs?= Date: Mon, 11 Apr 2022 10:18:22 +0200 Subject: [PATCH 1/4] Send newrelic events on login and register --- app/auth/views/login.py | 5 +++++ app/auth/views/register.py | 7 ++++++- app/events/auth_event.py | 31 +++++++++++++++++++++++++++++++ app/pgp_utils.py | 4 ++-- 4 files changed, 44 insertions(+), 3 deletions(-) create mode 100644 app/events/auth_event.py diff --git a/app/auth/views/login.py b/app/auth/views/login.py index c68ff887..a08ca774 100644 --- a/app/auth/views/login.py +++ b/app/auth/views/login.py @@ -5,6 +5,7 @@ from wtforms import StringField, validators from app.auth.base import auth_bp from app.auth.views.login_utils import after_login +from app.events.auth_event import LoginEvent from app.extensions import limiter from app.log import LOG from app.models import User @@ -43,18 +44,22 @@ def login(): g.deduct_limit = True form.password.data = None flash("Email or password incorrect", "error") + LoginEvent(LoginEvent.ActionType.failed).send() elif user.disabled: flash( "Your account is disabled. Please contact SimpleLogin team to re-enable your account.", "error", ) + LoginEvent(LoginEvent.ActionType.disabled_login).send() elif not user.activated: show_resend_activation = True flash( "Please check your inbox for the activation email. You can also have this email re-sent", "error", ) + LoginEvent(LoginEvent.ActionType.not_activated).send() else: + LoginEvent(LoginEvent.ActionType.success).send() return after_login(user, next_url) return render_template( diff --git a/app/auth/views/register.py b/app/auth/views/register.py index abbc56f5..5ed8b47d 100644 --- a/app/auth/views/register.py +++ b/app/auth/views/register.py @@ -13,6 +13,7 @@ from app.email_utils import ( email_can_be_used_as_mailbox, personal_email_already_used, ) +from app.events.auth_event import RegisterEvent from app.log import LOG from app.models import User, ActivationCode from app.utils import random_string, encode_url, sanitize_email @@ -60,6 +61,7 @@ def register(): hcaptcha_res, ) flash("Wrong Captcha", "error") + RegisterEvent(RegisterEvent.ActionType.regsiter_catpcha_failed).send() return render_template( "auth/register.html", form=form, @@ -70,10 +72,11 @@ def register(): email = sanitize_email(form.email.data) if not email_can_be_used_as_mailbox(email): flash("You cannot use this email address as your personal inbox.", "error") - + RegisterEvent(RegisterEvent.ActionType.email_in_use).send() else: if personal_email_already_used(email): flash(f"Email {email} already used", "error") + RegisterEvent(RegisterEvent.ActionType.email_in_use).send() else: LOG.d("create user %s", email) user = User.create( @@ -86,8 +89,10 @@ def register(): try: send_activation_email(user, next_url) + RegisterEvent(RegisterEvent.ActionType.success).send() except Exception: flash("Invalid email, are you sure the email is correct?", "error") + RegisterEvent(RegisterEvent.ActionType.invalid_email).send() return redirect(url_for("auth.register")) return render_template("auth/register_waiting_activation.html") diff --git a/app/events/auth_event.py b/app/events/auth_event.py new file mode 100644 index 00000000..8e2a830c --- /dev/null +++ b/app/events/auth_event.py @@ -0,0 +1,31 @@ +import newrelic + +from app.models import EnumE + + +class LoginEvent: + class ActionType(EnumE): + success = 0 + failed = 1 + disabled_login = 2 + not_activated = 3 + + def __init__(self, action: ActionType): + self.action = action + + def send(self): + newrelic.agent.record_custom_event("LoginEvent", {"action": self.action}) + + +class RegisterEvent: + class ActionType(EnumE): + success = 0 + catpcha_failed = 1 + email_in_use = 2 + invalid_email = 3 + + def __init__(self, action: ActionType): + self.action = action + + def send(self): + newrelic.agent.record_custom_event("RegisterEvent", {"action": self.action}) diff --git a/app/pgp_utils.py b/app/pgp_utils.py index 211ec00d..8934ddec 100644 --- a/app/pgp_utils.py +++ b/app/pgp_utils.py @@ -65,7 +65,7 @@ def encrypt_file(data: BytesIO, fingerprint: str) -> str: LOG.d("mem_usage %s", mem_usage) r = gpg.encrypt_file(data, fingerprint, always_trust=True) - if not r.ok: + if not r.success: # maybe the fingerprint is not loaded on this host, try to load it found = False # searching for the key in mailbox @@ -87,7 +87,7 @@ def encrypt_file(data: BytesIO, fingerprint: str) -> str: data.seek(0) r = gpg.encrypt_file(data, fingerprint, always_trust=True) - if not r.ok: + if not r.success: raise PGPException(f"Cannot encrypt, status: {r.status}") return str(r) From dc59b61fba6f936298a4074337ffb2e3da9bd587 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Adri=C3=A0=20Casaj=C3=BAs?= Date: Mon, 11 Apr 2022 10:20:02 +0200 Subject: [PATCH 2/4] Revert changes to pgp_utils --- app/pgp_utils.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/app/pgp_utils.py b/app/pgp_utils.py index 8934ddec..211ec00d 100644 --- a/app/pgp_utils.py +++ b/app/pgp_utils.py @@ -65,7 +65,7 @@ def encrypt_file(data: BytesIO, fingerprint: str) -> str: LOG.d("mem_usage %s", mem_usage) r = gpg.encrypt_file(data, fingerprint, always_trust=True) - if not r.success: + if not r.ok: # maybe the fingerprint is not loaded on this host, try to load it found = False # searching for the key in mailbox @@ -87,7 +87,7 @@ def encrypt_file(data: BytesIO, fingerprint: str) -> str: data.seek(0) r = gpg.encrypt_file(data, fingerprint, always_trust=True) - if not r.success: + if not r.ok: raise PGPException(f"Cannot encrypt, status: {r.status}") return str(r) From 8da4293305ee6bbfdc0a4c964a8ee3ebe990b178 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Adri=C3=A0=20Casaj=C3=BAs?= Date: Mon, 11 Apr 2022 16:04:28 +0200 Subject: [PATCH 3/4] typo --- app/auth/views/register.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/auth/views/register.py b/app/auth/views/register.py index 5ed8b47d..60e6b01d 100644 --- a/app/auth/views/register.py +++ b/app/auth/views/register.py @@ -61,7 +61,7 @@ def register(): hcaptcha_res, ) flash("Wrong Captcha", "error") - RegisterEvent(RegisterEvent.ActionType.regsiter_catpcha_failed).send() + RegisterEvent(RegisterEvent.ActionType.catpcha_failed).send() return render_template( "auth/register.html", form=form, From 2b149747f5f864eecd40803254caef0b7305b2d0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Adri=C3=A0=20Casaj=C3=BAs?= Date: Mon, 11 Apr 2022 16:11:01 +0200 Subject: [PATCH 4/4] Also track login and register events from the api routes --- app/api/views/auth.py | 12 ++++++++++++ app/events/auth_event.py | 29 ++++++++++++++++++++++------- 2 files changed, 34 insertions(+), 7 deletions(-) diff --git a/app/api/views/auth.py b/app/api/views/auth.py index 105e0bb6..a149f9b4 100644 --- a/app/api/views/auth.py +++ b/app/api/views/auth.py @@ -19,6 +19,7 @@ from app.email_utils import ( send_email, render, ) +from app.events.auth_event import LoginEvent, RegisterEvent from app.extensions import limiter from app.log import LOG from app.models import User, ApiKey, SocialAuth, AccountActivation @@ -55,16 +56,20 @@ def auth_login(): user = User.filter_by(email=email).first() if not user or not user.check_password(password): + LoginEvent(LoginEvent.ActionType.failed, LoginEvent.Source.api).send() return jsonify(error="Email or password incorrect"), 400 elif user.disabled: + LoginEvent(LoginEvent.ActionType.disabled_login, LoginEvent.Source.api).send() return jsonify(error="Account disabled"), 400 elif not user.activated: + LoginEvent(LoginEvent.ActionType.not_activated, LoginEvent.Source.api).send() return jsonify(error="Account not activated"), 422 elif user.fido_enabled(): # allow user who has TOTP enabled to continue using the mobile app if not user.enable_otp: return jsonify(error="Currently we don't support FIDO on mobile yet"), 403 + LoginEvent(LoginEvent.ActionType.success, LoginEvent.Source.api).send() return jsonify(**auth_payload(user, device)), 200 @@ -88,14 +93,20 @@ def auth_register(): password = data.get("password") if DISABLE_REGISTRATION: + RegisterEvent(RegisterEvent.ActionType.failed, RegisterEvent.Source.api).send() return jsonify(error="registration is closed"), 400 if not email_can_be_used_as_mailbox(email) or personal_email_already_used(email): + RegisterEvent( + RegisterEvent.ActionType.invalid_email, RegisterEvent.Source.api + ).send() return jsonify(error=f"cannot use {email} as personal inbox"), 400 if not password or len(password) < 8: + RegisterEvent(RegisterEvent.ActionType.failed, RegisterEvent.Source.api).send() return jsonify(error="password too short"), 400 if len(password) > 100: + RegisterEvent(RegisterEvent.ActionType.failed, RegisterEvent.Source.api).send() return jsonify(error="password too long"), 400 LOG.d("create user %s", email) @@ -114,6 +125,7 @@ def auth_register(): render("transactional/code-activation.html", code=code), ) + RegisterEvent(RegisterEvent.ActionType.success, RegisterEvent.Source.api).send() return jsonify(msg="User needs to confirm their account"), 200 diff --git a/app/events/auth_event.py b/app/events/auth_event.py index 8e2a830c..2ed9296e 100644 --- a/app/events/auth_event.py +++ b/app/events/auth_event.py @@ -10,22 +10,37 @@ class LoginEvent: disabled_login = 2 not_activated = 3 - def __init__(self, action: ActionType): + class Source(EnumE): + web = 0 + api = 1 + + def __init__(self, action: ActionType, source: Source = Source.web): self.action = action + self.source = source def send(self): - newrelic.agent.record_custom_event("LoginEvent", {"action": self.action}) + newrelic.agent.record_custom_event( + "LoginEvent", {"action": self.action, "source": self.source} + ) class RegisterEvent: class ActionType(EnumE): success = 0 - catpcha_failed = 1 - email_in_use = 2 - invalid_email = 3 + failed = 1 + catpcha_failed = 2 + email_in_use = 3 + invalid_email = 4 - def __init__(self, action: ActionType): + class Source(EnumE): + web = 0 + api = 1 + + def __init__(self, action: ActionType, source: Source = Source.web): self.action = action + self.source = source def send(self): - newrelic.agent.record_custom_event("RegisterEvent", {"action": self.action}) + newrelic.agent.record_custom_event( + "RegisterEvent", {"action": self.action, "source": self.source} + )