Show account names and roles on cards from scheduled scans

Cards from scheduled scans of editor-saved profiles showed the account's
GUID as its name and no Elev/Ansat badge: the profile editor stored bare
id strings, the scheduler passes them straight to run_scan, and run_scan
normalised them to displayName=<id>, userRole="other". Before 1.7.10 the
scan-start profile overwrite kept repairing profiles to named objects,
which is why this surfaced now.

- run_scan: _enrich_bare_users() looks bare accounts up in the directory
  (list_users, falling back to lookup_user) for name and role.
- M365Connector.build_role_sku_map(): the SKU map /api/users built inline,
  now shared so cards and the account list classify roles identically.
  lookup_user() also returns assignedLicenses.
- Profile editor saves {id, displayName, userRole} (manual accounts keep
  email/manual), like the 💾 save.
- _accountPill: an account_name equal to account_id counts as missing;
  name and role are filled from the loaded user list, so existing cards
  display correctly without a rescan.
- Excel export: the scan's account-name map no longer breaks on bare id
  strings (.get() on a str raised and dropped the whole map).

Tests: tests/test_account_names.py (10); the run_scan test fails on the
old engine with "Collecting emails — guid-henrik…".

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
StyxX65andClaude Opus 5.5 committed 2026-09-28 14:40:14 +02:00
1 parent c55db48894
commit 20156ffc93
8 files changed
+283 -37

No files matched your search

+44
View File
@@ -87,6 +87,49 @@ def _resolve_scan_users(conn, user_ids: list[dict]) -> list[dict]:
return resolved
def _enrich_bare_users(conn, user_ids: list[dict]) -> list[dict]:
"""Fill in displayName and userRole for accounts given only by id.
Profiles saved by the profile editor (and scheduled scans of them) pass
plain id strings, which run_scan normalises to displayName=<id>,
userRole="other" — so their cards showed the GUID as the name and no
Elev/Ansat badge. Look those accounts up in the directory once, using the
same role classification as GET /api/users. Accounts that can't be looked
up keep what they had.
"""
bare = {id(u) for u in user_ids
if not u.get("displayName") or u.get("displayName") == u.get("id")}
if not bare:
return user_ids
directory: dict[str, dict] = {}
sku_map: dict = {}
try:
users = conn.list_users()
directory = {u["id"]: u for u in users if u.get("id")}
sku_map = conn.build_role_sku_map(users)
except Exception as e: # e.g. delegated mode without User.Read.All
logger.info("[run_scan] directory list unavailable for name lookup: %s", e)
enriched = []
for u in user_ids:
if id(u) in bare:
uid = u.get("id", "")
d = directory.get(uid)
if d is None:
try:
d = conn.lookup_user(uid) # not in the licensed-member list
except Exception:
d = None
if d:
_email = d.get("mail") or d.get("userPrincipalName", "")
u = {**u, "displayName": _resolve_display_name(d.get("displayName", ""), _email, uid)}
if d.get("assignedLicenses") is not None and u.get("userRole", "other") == "other":
u["userRole"] = conn.classify_user_role(d.get("assignedLicenses", []), sku_map)
enriched.append(u)
return enriched
def _get_scan_abort():
return _state._scan_abort
@@ -686,6 +729,7 @@ def run_scan(options: dict):
me_info.get("mail") or me_info.get("userPrincipalName", "me"))}]
else:
user_ids = _resolve_scan_users(conn, user_ids)
user_ids = _enrich_bare_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)