Keep Google-only accounts out of the M365 scan

Graph returned 404 for /users/google:<email>/mailFolders: Google-
Workspace-only accounts (id "google:<email>") reached the M365 scan.

- getSelectedUsers() fell back to "every ticked checkbox in the DOM"
  whenever no M365 account was ticked — including Google-only rows. The
  fallback is now only used before the user list has loaded, and never
  includes google: rows.
- startScan() skips the M365 part (with a log line) when M365 sources are
  ticked but no M365 accounts are: the server treats an empty account
  list as "every user in the tenant".
- run_scan: _resolve_manual_users -> _resolve_scan_users, which now also
  drops google: ids (reported as a phase message). Covers scheduled scans
  and profiles that already contain Google-only accounts. Never widens to
  all users.

Tests: 3 new in tests/test_manual_users.py; the run_scan one fails on the
old engine with exactly the reported Graph call.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
StyxX65andClaude Opus 5.5 committed 2026-09-28 10:28:05 +02:00
1 parent b60c4a5c47
commit b5476228ba
8 files changed
+99 -19

No files matched your search

+21 -6
View File
@@ -42,18 +42,27 @@ except ImportError:
from routes import state as _state
_MANUAL_PREFIX = "manual:"
_GOOGLE_PREFIX = "google:"
def _resolve_manual_users(conn, user_ids: list[dict]) -> list[dict]:
"""Resolve profile accounts saved as "manual:<email>" to their directory id.
def _resolve_scan_users(conn, user_ids: list[dict]) -> list[dict]:
"""Turn the requested accounts into Graph user ids for the M365 scan.
Older profile-editor saves (and saves made while M365 was not connected)
store manual accounts by email only; Graph needs the user id. Accounts that
can't be resolved are reported and skipped — never widened to "all users".
- "manual:<email>" (older profile-editor saves, or saves made while M365
was not connected) is looked up to get the directory id.
- "google:<email>" is a Google-Workspace-only account: it has no M365
mailbox/drive and Graph 404s on it. Profiles and older UI selections can
contain them, so they are dropped here (the Google scan covers them).
Accounts that can't be used are reported and skipped — never widened to
"all users".
"""
resolved, seen = [], set()
skipped_google = []
for u in user_ids:
uid = u.get("id", "")
if uid.startswith(_GOOGLE_PREFIX):
skipped_google.append(uid[len(_GOOGLE_PREFIX):])
continue
if uid.startswith(_MANUAL_PREFIX):
upn = uid[len(_MANUAL_PREFIX):]
try:
@@ -69,6 +78,12 @@ def _resolve_manual_users(conn, user_ids: list[dict]) -> list[dict]:
continue
seen.add(u["id"])
resolved.append(u)
if skipped_google:
logger.info("[run_scan] skipped %d Google-only account(s) in M365 scan", len(skipped_google))
broadcast("scan_phase", {"phase": LANG.get(
"m365_skipped_google_accounts",
"Skipped {n} Google Workspace-only account(s) — not in Microsoft 365").replace(
"{n}", str(len(skipped_google)))})
return resolved
@@ -670,7 +685,7 @@ def run_scan(options: dict):
me_info.get("displayName", ""),
me_info.get("mail") or me_info.get("userPrincipalName", "me"))}]
else:
user_ids = _resolve_manual_users(conn, user_ids)
user_ids = _resolve_scan_users(conn, user_ids)
sample = user_ids[0] if user_ids else None
logger.info("[run_scan] user_ids: %d entries, type=%s, sample=%s",
len(user_ids), type(user_ids).__name__, sample)