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:
1 parent
c55db48894
commit
20156ffc93
8 files changed
+283
-37
No files matched your search
@@ -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)
|
||||
|
||||
Reference in new issue
Block a user