From 4c817e77d5a0ca27c4cf1d14e1a125149032a016 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jakub=20=C5=A0iagi?= Date: Wed, 19 Aug 2026 19:01:22 +0200 Subject: [PATCH 1/6] feat: send alerts to users without Google TFA --- trojstenid/settings.py | 3 + .../users/{google_groups.py => google_api.py} | 47 ++++++++++++- .../management/commands/sendtfaalerts.py | 70 +++++++++++++++++++ .../management/commands/syncgooglegroups.py | 2 +- trojstenid/users/tasks.py | 2 +- .../templates/account/email/tfa_alert.html | 7 ++ 6 files changed, 128 insertions(+), 3 deletions(-) rename trojstenid/users/{google_groups.py => google_api.py} (61%) create mode 100644 trojstenid/users/management/commands/sendtfaalerts.py create mode 100644 trojstenid/users/templates/account/email/tfa_alert.html diff --git a/trojstenid/settings.py b/trojstenid/settings.py index 6ab002c..cb798cc 100644 --- a/trojstenid/settings.py +++ b/trojstenid/settings.py @@ -11,6 +11,7 @@ """ from base64 import b64decode +from datetime import timedelta from pathlib import Path from environs import Env @@ -249,6 +250,8 @@ GOOGLE_ADMIN_SERVICE_ACCOUNT = env("GOOGLE_ADMIN_SERVICE_ACCOUNT", default="") GOOGLE_ADMIN_SUBJECT = env("GOOGLE_ADMIN_SUBJECT", default="") +GOOGLE_TFA_MIN_ACCOUNT_AGE = env.timedelta("GOOGLE_TFA_MIN_ACCOUNT_AGE", default=timedelta(days=5)) +GOOGLE_TFA_MAX_ACCOUNT_AGE = env.timedelta("GOOGLE_TFA_MAX_ACCOUNT_AGE", default=timedelta(days=14)) GITHUB_APP_ID = env.int("GITHUB_APP_ID", default=-1) GITHUB_APP_PRIVATE_KEY = b64decode(env("GITHUB_APP_PRIVATE_KEY", default="")).decode() diff --git a/trojstenid/users/google_groups.py b/trojstenid/users/google_api.py similarity index 61% rename from trojstenid/users/google_groups.py rename to trojstenid/users/google_api.py index 48b4033..de80089 100644 --- a/trojstenid/users/google_groups.py +++ b/trojstenid/users/google_api.py @@ -1,16 +1,21 @@ import json import logging +from datetime import datetime, timedelta from allauth.account.models import EmailAddress +from dateutil.parser import isoparse from django.conf import settings from django.contrib.auth.models import Group +from django.utils.timezone import now from google.oauth2 import service_account from googleapiclient.discovery import build logger = logging.getLogger(__name__) -SCOPES = ["https://www.googleapis.com/auth/admin.directory.group.readonly"] +SCOPES = ["https://www.googleapis.com/auth/admin.directory.group.readonly", + "https://www.googleapis.com/auth/admin.directory.user.readonly"] IAM_DOMAIN = "iam.trojsten.sk" +TFA_ENFORCED_AFTER = timedelta(days=14) def _get_credentials(): @@ -103,3 +108,43 @@ def sync_iam_groups() -> None: """ for group in fetch_iam_google_groups(): sync_group(group) + + +def query_nontfa_users() -> list[tuple[str, datetime]]: + """ + Returns all email addresses of users whose account is at least one-week-old and has not enabled 2FA. + """ + min_account_age = getattr(settings, "GOOGLE_TFA_MIN_ACCOUNT_AGE") + max_account_age = getattr(settings, "GOOGLE_TFA_MAX_ACCOUNT_AGE") + if min_account_age is None or max_account_age is None: + logger.warning("Google TFA account age range not configured") + return [] + + credentials = _get_credentials() + if credentials is None: + logger.warning("Google Admin service account not configured") + return [] + + users = [] + + directory = build("admin", "directory_v1", credentials=credentials) + request = directory.users().list(customer="my_customer", orderBy="email") + while request: + response = request.execute() + + for user in response.get("users", []): + email = user.get("primaryEmail", "") + has_2sv_enabled = user.get("isEnrolledIn2Sv", False) + has_2sv_enforced = user.get("isEnforcedIn2Sv", False) + creation_time = isoparse(user["creationTime"]) if "creationTime" in user else None + if ( + not has_2sv_enabled + and has_2sv_enforced + and creation_time + ): + account_age = now() - creation_time + if min_account_age <= account_age <= max_account_age: + users.append((email, creation_time + TFA_ENFORCED_AFTER)) + + request = directory.users().list_next(request, response) + return users diff --git a/trojstenid/users/management/commands/sendtfaalerts.py b/trojstenid/users/management/commands/sendtfaalerts.py new file mode 100644 index 0000000..598f67f --- /dev/null +++ b/trojstenid/users/management/commands/sendtfaalerts.py @@ -0,0 +1,70 @@ +from django.conf import settings +from django.core.mail import send_mail +from django.core.management import BaseCommand +from django.template.loader import render_to_string + +from trojstenid.users.google_api import query_nontfa_users + + +class Command(BaseCommand): + help = "Send mail alerts to Google users without TFA enabled" + + def add_arguments(self, parser): + parser.add_argument( + "--dry-run", + action="store_true", + help="Run without actually sending emails", + ) + + def handle(self, *args, **options): + dry_run = options["dry_run"] + nontfa_users = query_nontfa_users() + + if not nontfa_users: + self.stdout.write(self.style.NOTICE("No users found without 2FA enabled.")) + return + + self.stdout.write(self.style.MIGRATE_HEADING(f"Found {len(nontfa_users)} user(s) requiring 2FA alert.\n")) + + sent_count = 0 + failed_count = 0 + + for email, required_after in nontfa_users: + if dry_run: + self.stdout.write( + self.style.NOTICE(f"[DRY-RUN] Would alert {email:<32} | 2FA required after: {required_after}") + ) + continue + + try: + content = render_to_string( + "account/email/tfa_alert.html", + {"email": email, "required_after": required_after}, + ) + + send_mail( + subject="Dvojstupňové overenie tvojho Trojsten Google účtu", + message=content, + from_email=settings.DEFAULT_FROM_EMAIL, + recipient_list=[email], + ) + sent_count += 1 + self.stdout.write( + self.style.SUCCESS(f"Sent to '{email}', 2FA required after: {required_after}") + ) + except Exception as e: + failed_count += 1 + self.stderr.write( + self.style.ERROR(f"Failed to send to '{email}', Error: {e}") + ) + + + self.stdout.write("") + if dry_run: + self.stdout.write(self.style.SUCCESS(f"Dry run complete.")) + else: + self.stdout.write( + self.style.SUCCESS( + f"Completed. Sent: {sent_count}, failed: {failed_count}" + ) + ) \ No newline at end of file diff --git a/trojstenid/users/management/commands/syncgooglegroups.py b/trojstenid/users/management/commands/syncgooglegroups.py index 0f25915..96931d3 100644 --- a/trojstenid/users/management/commands/syncgooglegroups.py +++ b/trojstenid/users/management/commands/syncgooglegroups.py @@ -2,7 +2,7 @@ from django.core.management.base import BaseCommand -from trojstenid.users.google_groups import sync_iam_groups +from trojstenid.users.google_api import sync_iam_groups class Command(BaseCommand): diff --git a/trojstenid/users/tasks.py b/trojstenid/users/tasks.py index 01ddfd0..28015a9 100644 --- a/trojstenid/users/tasks.py +++ b/trojstenid/users/tasks.py @@ -5,7 +5,7 @@ from django_rq import job from trojstenid.users.github import sync_github_teams -from trojstenid.users.google_groups import sync_iam_groups +from trojstenid.users.google_api import sync_iam_groups from trojstenid.users.models import Application, User from trojstenid.users.serializers import UserSerializer diff --git a/trojstenid/users/templates/account/email/tfa_alert.html b/trojstenid/users/templates/account/email/tfa_alert.html new file mode 100644 index 0000000..61d3ee0 --- /dev/null +++ b/trojstenid/users/templates/account/email/tfa_alert.html @@ -0,0 +1,7 @@ +{% extends "account/email/base_message.txt" %} + +{% block content %}V tvojom Trojstenovom Google účte si si ešte nenastavil dvojstupňové overovanie (2FA). +Do dvoch týždňov od vytvorenie účtu si musíš nastaviť dvojstupňové overovanie (v tvojom prípade do {{ required_after|date:"d.m.Y" }}), inak stratíš prístup k účtu. + +Návod na to, ako to urobiť vieš nájsť napr. tu: https://support.google.com/accounts/answer/185839?hl=sk +{% endblock content %} From 687ff18451fe73b31f94575321e3f1a8749567e2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jakub=20=C5=A0iagi?= Date: Thu, 20 Aug 2026 14:38:35 +0200 Subject: [PATCH 2/6] feat: add secondary email cc and reword email --- trojstenid/settings.py | 8 +- trojstenid/users/google_api.py | 26 ++++-- .../management/commands/sendtfaalerts.py | 92 +++++++++++-------- .../templates/account/email/tfa_alert.html | 7 -- .../templates/account/email/tfa_alert.txt | 10 ++ 5 files changed, 87 insertions(+), 56 deletions(-) delete mode 100644 trojstenid/users/templates/account/email/tfa_alert.html create mode 100644 trojstenid/users/templates/account/email/tfa_alert.txt diff --git a/trojstenid/settings.py b/trojstenid/settings.py index cb798cc..c0b7add 100644 --- a/trojstenid/settings.py +++ b/trojstenid/settings.py @@ -250,8 +250,12 @@ GOOGLE_ADMIN_SERVICE_ACCOUNT = env("GOOGLE_ADMIN_SERVICE_ACCOUNT", default="") GOOGLE_ADMIN_SUBJECT = env("GOOGLE_ADMIN_SUBJECT", default="") -GOOGLE_TFA_MIN_ACCOUNT_AGE = env.timedelta("GOOGLE_TFA_MIN_ACCOUNT_AGE", default=timedelta(days=5)) -GOOGLE_TFA_MAX_ACCOUNT_AGE = env.timedelta("GOOGLE_TFA_MAX_ACCOUNT_AGE", default=timedelta(days=14)) +GOOGLE_TFA_MIN_ACCOUNT_AGE = env.timedelta( + "GOOGLE_TFA_MIN_ACCOUNT_AGE", default=timedelta(days=5) +) +GOOGLE_TFA_MAX_ACCOUNT_AGE = env.timedelta( + "GOOGLE_TFA_MAX_ACCOUNT_AGE", default=timedelta(days=14) +) GITHUB_APP_ID = env.int("GITHUB_APP_ID", default=-1) GITHUB_APP_PRIVATE_KEY = b64decode(env("GITHUB_APP_PRIVATE_KEY", default="")).decode() diff --git a/trojstenid/users/google_api.py b/trojstenid/users/google_api.py index de80089..c93ebc5 100644 --- a/trojstenid/users/google_api.py +++ b/trojstenid/users/google_api.py @@ -12,8 +12,10 @@ logger = logging.getLogger(__name__) -SCOPES = ["https://www.googleapis.com/auth/admin.directory.group.readonly", - "https://www.googleapis.com/auth/admin.directory.user.readonly"] +SCOPES = [ + "https://www.googleapis.com/auth/admin.directory.group.readonly", + "https://www.googleapis.com/auth/admin.directory.user.readonly", +] IAM_DOMAIN = "iam.trojsten.sk" TFA_ENFORCED_AFTER = timedelta(days=14) @@ -110,7 +112,7 @@ def sync_iam_groups() -> None: sync_group(group) -def query_nontfa_users() -> list[tuple[str, datetime]]: +def query_nontfa_users() -> list[tuple[str, datetime, str | None]]: """ Returns all email addresses of users whose account is at least one-week-old and has not enabled 2FA. """ @@ -136,15 +138,19 @@ def query_nontfa_users() -> list[tuple[str, datetime]]: email = user.get("primaryEmail", "") has_2sv_enabled = user.get("isEnrolledIn2Sv", False) has_2sv_enforced = user.get("isEnforcedIn2Sv", False) - creation_time = isoparse(user["creationTime"]) if "creationTime" in user else None - if ( - not has_2sv_enabled - and has_2sv_enforced - and creation_time - ): + creation_time = ( + isoparse(user["creationTime"]) if "creationTime" in user else None + ) + if not has_2sv_enabled and has_2sv_enforced and creation_time: account_age = now() - creation_time if min_account_age <= account_age <= max_account_age: - users.append((email, creation_time + TFA_ENFORCED_AFTER)) + users.append( + ( + email, + creation_time + TFA_ENFORCED_AFTER, + user.get("secondaryEmail", None), + ) + ) request = directory.users().list_next(request, response) return users diff --git a/trojstenid/users/management/commands/sendtfaalerts.py b/trojstenid/users/management/commands/sendtfaalerts.py index 598f67f..b3f925a 100644 --- a/trojstenid/users/management/commands/sendtfaalerts.py +++ b/trojstenid/users/management/commands/sendtfaalerts.py @@ -1,5 +1,5 @@ from django.conf import settings -from django.core.mail import send_mail +from django.core.mail import EmailMultiAlternatives, get_connection from django.core.management import BaseCommand from django.template.loader import render_to_string @@ -24,47 +24,65 @@ def handle(self, *args, **options): self.stdout.write(self.style.NOTICE("No users found without 2FA enabled.")) return - self.stdout.write(self.style.MIGRATE_HEADING(f"Found {len(nontfa_users)} user(s) requiring 2FA alert.\n")) + self.stdout.write( + self.style.MIGRATE_HEADING( + f"Found {len(nontfa_users)} user(s) requiring 2FA alert.\n" + ) + ) + + if dry_run: + for email, required_after, _ in nontfa_users: + self.stdout.write( + self.style.NOTICE( + f"[DRY-RUN] Would alert {email}, 2FA required after: {required_after}" + ) + ) + self.stdout.write(self.style.SUCCESS("\nDry run complete.")) + return sent_count = 0 failed_count = 0 - for email, required_after in nontfa_users: - if dry_run: - self.stdout.write( - self.style.NOTICE(f"[DRY-RUN] Would alert {email:<32} | 2FA required after: {required_after}") - ) - continue + connection = get_connection() + try: + connection.open() - try: - content = render_to_string( - "account/email/tfa_alert.html", - {"email": email, "required_after": required_after}, - ) + for email, required_after, secondary_email in nontfa_users: + try: + content = render_to_string( + "account/email/tfa_alert.txt", + {"email": email, "required_after": required_after}, + ) - send_mail( - subject="Dvojstupňové overenie tvojho Trojsten Google účtu", - message=content, - from_email=settings.DEFAULT_FROM_EMAIL, - recipient_list=[email], - ) - sent_count += 1 - self.stdout.write( - self.style.SUCCESS(f"Sent to '{email}', 2FA required after: {required_after}") - ) - except Exception as e: - failed_count += 1 - self.stderr.write( - self.style.ERROR(f"Failed to send to '{email}', Error: {e}") - ) + msg = EmailMultiAlternatives( + subject="Dvojstupňové overenie tvojho Trojsten Google účtu", + body=content, + from_email=settings.DEFAULT_FROM_EMAIL, + to=[email], + cc=[secondary_email] if secondary_email else None, + connection=connection, + ) + msg.send() + sent_count += 1 + self.stdout.write( + self.style.SUCCESS( + f"Sent to '{email}', 2FA required after: {required_after}" + ) + ) + except Exception as e: + failed_count += 1 + self.stderr.write( + self.style.ERROR(f"Failed to send to '{email}', Error: {e}") + ) - self.stdout.write("") - if dry_run: - self.stdout.write(self.style.SUCCESS(f"Dry run complete.")) - else: - self.stdout.write( - self.style.SUCCESS( - f"Completed. Sent: {sent_count}, failed: {failed_count}" - ) - ) \ No newline at end of file + except Exception as e: + self.stderr.write(self.style.ERROR(f"Error during email delivery: {e}")) + finally: + connection.close() + + self.stdout.write( + self.style.SUCCESS( + f"\nCompleted. Sent: {sent_count}, failed: {failed_count}" + ) + ) diff --git a/trojstenid/users/templates/account/email/tfa_alert.html b/trojstenid/users/templates/account/email/tfa_alert.html deleted file mode 100644 index 61d3ee0..0000000 --- a/trojstenid/users/templates/account/email/tfa_alert.html +++ /dev/null @@ -1,7 +0,0 @@ -{% extends "account/email/base_message.txt" %} - -{% block content %}V tvojom Trojstenovom Google účte si si ešte nenastavil dvojstupňové overovanie (2FA). -Do dvoch týždňov od vytvorenie účtu si musíš nastaviť dvojstupňové overovanie (v tvojom prípade do {{ required_after|date:"d.m.Y" }}), inak stratíš prístup k účtu. - -Návod na to, ako to urobiť vieš nájsť napr. tu: https://support.google.com/accounts/answer/185839?hl=sk -{% endblock content %} diff --git a/trojstenid/users/templates/account/email/tfa_alert.txt b/trojstenid/users/templates/account/email/tfa_alert.txt new file mode 100644 index 0000000..f3d0737 --- /dev/null +++ b/trojstenid/users/templates/account/email/tfa_alert.txt @@ -0,0 +1,10 @@ +{% extends "account/email/base_message.txt" %} + +{% block content %}Pred pár dňami ti bol vytvorený nový Trojstenový Google účet ({{ email }}). + +Tento email ti prichádza preto, že si si v ňom ešte nenastavil dvojstupňové overovanie (2FA). +Do dvoch týždňov od vytvorenie účtu si musíš nastaviť dvojstupňové overovanie (pre teba najnsekôr {{ required_after|date:"d.m.Y" }} do {{ required_after|time:"H:i" }}), inak stratíš prístup k účtu. + +Návod na to, ako to urobiť vieš nájsť napr. tu: https://support.google.com/accounts/answer/185839?hl=sk +Ak to dovtedy nestihneš a nebudeš sa vedieť prihlásiť do účtu, napíš nám na roots@trojsten.sk a vyriešime to. +{% endblock content %} From f1f1b74e707637f4b0f1fc279b964897a0b4187a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jakub=20=C5=A0iagi?= Date: Thu, 20 Aug 2026 14:49:51 +0200 Subject: [PATCH 3/6] refactor: make tfa enrollment period configurable --- trojstenid/settings.py | 3 +++ trojstenid/users/google_api.py | 10 +++++----- 2 files changed, 8 insertions(+), 5 deletions(-) diff --git a/trojstenid/settings.py b/trojstenid/settings.py index c0b7add..fdd4a7a 100644 --- a/trojstenid/settings.py +++ b/trojstenid/settings.py @@ -256,6 +256,9 @@ GOOGLE_TFA_MAX_ACCOUNT_AGE = env.timedelta( "GOOGLE_TFA_MAX_ACCOUNT_AGE", default=timedelta(days=14) ) +GOOGLE_TFA_ENROLLMENT_PERIOD = env.timedelta( + "GOOGLE_TFA_ENROLLMENT_PERIOD", default=timedelta(days=14) +) GITHUB_APP_ID = env.int("GITHUB_APP_ID", default=-1) GITHUB_APP_PRIVATE_KEY = b64decode(env("GITHUB_APP_PRIVATE_KEY", default="")).decode() diff --git a/trojstenid/users/google_api.py b/trojstenid/users/google_api.py index c93ebc5..adb1238 100644 --- a/trojstenid/users/google_api.py +++ b/trojstenid/users/google_api.py @@ -1,6 +1,6 @@ import json import logging -from datetime import datetime, timedelta +from datetime import datetime from allauth.account.models import EmailAddress from dateutil.parser import isoparse @@ -17,7 +17,6 @@ "https://www.googleapis.com/auth/admin.directory.user.readonly", ] IAM_DOMAIN = "iam.trojsten.sk" -TFA_ENFORCED_AFTER = timedelta(days=14) def _get_credentials(): @@ -118,8 +117,9 @@ def query_nontfa_users() -> list[tuple[str, datetime, str | None]]: """ min_account_age = getattr(settings, "GOOGLE_TFA_MIN_ACCOUNT_AGE") max_account_age = getattr(settings, "GOOGLE_TFA_MAX_ACCOUNT_AGE") - if min_account_age is None or max_account_age is None: - logger.warning("Google TFA account age range not configured") + enrollment_period = getattr(settings, "GOOGLE_TFA_ENROLLMENT_PERIOD") + if min_account_age is None or max_account_age is None or enrollment_period is None: + logger.warning("Google TFA account age or enrollment period not configured") return [] credentials = _get_credentials() @@ -147,7 +147,7 @@ def query_nontfa_users() -> list[tuple[str, datetime, str | None]]: users.append( ( email, - creation_time + TFA_ENFORCED_AFTER, + creation_time + enrollment_period, user.get("secondaryEmail", None), ) ) From 5b42845a1a39d0ff2ca81fe50b81b5c5f9908d36 Mon Sep 17 00:00:00 2001 From: Aiq0 <66842415+Aiq0@users.noreply.github.com> Date: Thu, 20 Aug 2026 18:32:37 +0200 Subject: [PATCH 4/6] feat: better email wording --- trojstenid/users/templates/account/email/tfa_alert.txt | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/trojstenid/users/templates/account/email/tfa_alert.txt b/trojstenid/users/templates/account/email/tfa_alert.txt index f3d0737..95da911 100644 --- a/trojstenid/users/templates/account/email/tfa_alert.txt +++ b/trojstenid/users/templates/account/email/tfa_alert.txt @@ -2,9 +2,12 @@ {% block content %}Pred pár dňami ti bol vytvorený nový Trojstenový Google účet ({{ email }}). -Tento email ti prichádza preto, že si si v ňom ešte nenastavil dvojstupňové overovanie (2FA). -Do dvoch týždňov od vytvorenie účtu si musíš nastaviť dvojstupňové overovanie (pre teba najnsekôr {{ required_after|date:"d.m.Y" }} do {{ required_after|time:"H:i" }}), inak stratíš prístup k účtu. +Tento email ti prichádza preto, že si si v ňom ešte nenastavil dvojstupňové overenie (2FA). +Do dvoch týždňov od vytvorenie účtu si musíš nastaviť dvojstupňové overenie (pre teba najneskôr {{ required_after|date:"d.m.Y" }} do {{ required_after|time:"H:i" }}), inak stratíš prístup k účtu. + +Návod na to, ako to urobiť, nájdeš tu: + +https://support.google.com/accounts/answer/185839?hl=sk -Návod na to, ako to urobiť vieš nájsť napr. tu: https://support.google.com/accounts/answer/185839?hl=sk Ak to dovtedy nestihneš a nebudeš sa vedieť prihlásiť do účtu, napíš nám na roots@trojsten.sk a vyriešime to. {% endblock content %} From 2c7b6503bc2c35db6c48cffa88602ba47855d4fa Mon Sep 17 00:00:00 2001 From: Aiq0 <66842415+Aiq0@users.noreply.github.com> Date: Thu, 20 Aug 2026 18:41:05 +0200 Subject: [PATCH 5/6] feat: delegate some filters to google api and fix secondary emails --- trojstenid/users/google_api.py | 20 ++++++++++++------- .../management/commands/sendtfaalerts.py | 2 +- 2 files changed, 14 insertions(+), 8 deletions(-) diff --git a/trojstenid/users/google_api.py b/trojstenid/users/google_api.py index adb1238..87bd41c 100644 --- a/trojstenid/users/google_api.py +++ b/trojstenid/users/google_api.py @@ -111,9 +111,9 @@ def sync_iam_groups() -> None: sync_group(group) -def query_nontfa_users() -> list[tuple[str, datetime, str | None]]: +def query_nontfa_users() -> list[tuple[str, datetime, list[str]]]: """ - Returns all email addresses of users whose account is at least one-week-old and has not enabled 2FA. + Returns all email addresses of users whose account is new and has not enabled 2FA. """ min_account_age = getattr(settings, "GOOGLE_TFA_MIN_ACCOUNT_AGE") max_account_age = getattr(settings, "GOOGLE_TFA_MAX_ACCOUNT_AGE") @@ -130,25 +130,31 @@ def query_nontfa_users() -> list[tuple[str, datetime, str | None]]: users = [] directory = build("admin", "directory_v1", credentials=credentials) - request = directory.users().list(customer="my_customer", orderBy="email") + request = directory.users().list( + customer="my_customer", + orderBy="email", + query="isEnrolledIn2Sv=false isEnforcedIn2Sv=true", + ) while request: response = request.execute() for user in response.get("users", []): email = user.get("primaryEmail", "") - has_2sv_enabled = user.get("isEnrolledIn2Sv", False) - has_2sv_enforced = user.get("isEnforcedIn2Sv", False) creation_time = ( isoparse(user["creationTime"]) if "creationTime" in user else None ) - if not has_2sv_enabled and has_2sv_enforced and creation_time: + if creation_time: account_age = now() - creation_time if min_account_age <= account_age <= max_account_age: users.append( ( email, creation_time + enrollment_period, - user.get("secondaryEmail", None), + [ + e.get("address") + for e in user.get("emails", []) + if "address" in e and not e.get("primary", False) + ], ) ) diff --git a/trojstenid/users/management/commands/sendtfaalerts.py b/trojstenid/users/management/commands/sendtfaalerts.py index b3f925a..bf97643 100644 --- a/trojstenid/users/management/commands/sendtfaalerts.py +++ b/trojstenid/users/management/commands/sendtfaalerts.py @@ -59,7 +59,7 @@ def handle(self, *args, **options): body=content, from_email=settings.DEFAULT_FROM_EMAIL, to=[email], - cc=[secondary_email] if secondary_email else None, + cc=secondary_email if secondary_email else None, connection=connection, ) msg.send() From 0b451ca491d705812ad10fd771af859966eb4be6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jakub=20=C5=A0iagi?= Date: Fri, 11 Sep 2026 13:20:40 +0200 Subject: [PATCH 6/6] fix: remove non-null checks --- trojstenid/users/google_api.py | 3 --- 1 file changed, 3 deletions(-) diff --git a/trojstenid/users/google_api.py b/trojstenid/users/google_api.py index 87bd41c..a81f826 100644 --- a/trojstenid/users/google_api.py +++ b/trojstenid/users/google_api.py @@ -118,9 +118,6 @@ def query_nontfa_users() -> list[tuple[str, datetime, list[str]]]: min_account_age = getattr(settings, "GOOGLE_TFA_MIN_ACCOUNT_AGE") max_account_age = getattr(settings, "GOOGLE_TFA_MAX_ACCOUNT_AGE") enrollment_period = getattr(settings, "GOOGLE_TFA_ENROLLMENT_PERIOD") - if min_account_age is None or max_account_age is None or enrollment_period is None: - logger.warning("Google TFA account age or enrollment period not configured") - return [] credentials = _get_credentials() if credentials is None: