Scan manually added accounts
Build — Windows, Linux & macOS / GDPRScanner / linux (push) Has been cancelled
Build — Windows, Linux & macOS / GDPRScanner / macos (push) Has been cancelled
Build — Windows, Linux & macOS / GDPRScanner / windows (push) Has been cancelled
Build — Windows, Linux & macOS / Create GitHub Release (push) Has been cancelled
Build — Windows, Linux & macOS / GDPRScanner / linux (push) Has been cancelled
Build — Windows, Linux & macOS / GDPRScanner / macos (push) Has been cancelled
Build — Windows, Linux & macOS / GDPRScanner / windows (push) Has been cancelled
Build — Windows, Linux & macOS / Create GitHub Release (push) Has been cancelled
Manual accounts were never scanned: - Sidebar "add user" entries had no platform, and getSelectedUsers() required platform === 'm365', so they were dropped from every scan. - Profile-editor manual accounts were stored as "manual:<email>", which is not a Graph user id, and applying the profile never put them in the sidebar list, so they could not be selected. Now the editor looks the email up via /api/users/lookup and stores the directory id plus name/email (falling back to manual:<email> only when the lookup fails). Applying a profile adds its manual accounts to the sidebar and resolves legacy manual:<email> entries, logging any that can't be found. The scan engine resolves remaining manual:<email> ids (e.g. from scheduled scans) via the new M365Connector.lookup_user(), skipping and reporting failures without widening to all users. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit was merged in pull request #1.
This commit is contained in:
1 parent
bbe9c62ff9
commit
1c79ac15b7
11 files changed
+234
-18
No files matched your search
@@ -0,0 +1,104 @@
|
||||
"""
|
||||
Manual accounts in scan profiles.
|
||||
|
||||
The profile editor stores manually added accounts either with their directory
|
||||
id (looked up via GET /api/users/lookup) or, when M365 wasn't reachable / for
|
||||
profiles saved by older versions, as "manual:<email>". The scan engine must
|
||||
resolve the latter to a Graph user id or they are never scanned.
|
||||
"""
|
||||
from __future__ import annotations
|
||||
from unittest.mock import MagicMock
|
||||
|
||||
import pytest
|
||||
|
||||
import scan_engine
|
||||
|
||||
|
||||
class _Conn:
|
||||
def __init__(self, directory: dict[str, dict]):
|
||||
self.directory = directory
|
||||
self.lookups: list[str] = []
|
||||
|
||||
def lookup_user(self, upn: str) -> dict:
|
||||
self.lookups.append(upn)
|
||||
if upn not in self.directory:
|
||||
raise RuntimeError("Request_ResourceNotFound")
|
||||
return self.directory[upn]
|
||||
|
||||
|
||||
@pytest.fixture()
|
||||
def broadcasts(monkeypatch):
|
||||
sent = []
|
||||
monkeypatch.setattr(scan_engine, "broadcast", lambda ev, data: sent.append((ev, data)))
|
||||
return sent
|
||||
|
||||
|
||||
class TestResolveManualUsers:
|
||||
def test_manual_email_resolved_to_directory_id(self, broadcasts):
|
||||
conn = _Conn({"ann@school.dk": {"id": "guid-ann", "displayName": "Ann Andersen",
|
||||
"mail": "ann@school.dk"}})
|
||||
out = scan_engine._resolve_manual_users(conn, [
|
||||
{"id": "manual:ann@school.dk", "displayName": "manual:ann@school.dk", "userRole": "other"},
|
||||
])
|
||||
assert out == [{"id": "guid-ann", "displayName": "Ann Andersen", "userRole": "other"}]
|
||||
assert conn.lookups == ["ann@school.dk"]
|
||||
assert broadcasts == []
|
||||
|
||||
def test_regular_and_resolved_manual_ids_pass_through_without_lookup(self, broadcasts):
|
||||
conn = _Conn({})
|
||||
users = [{"id": "guid-1", "displayName": "One"},
|
||||
{"id": "guid-2", "displayName": "Two", "manual": True, "email": "two@school.dk"}]
|
||||
assert scan_engine._resolve_manual_users(conn, users) == users
|
||||
assert conn.lookups == []
|
||||
|
||||
def test_unknown_manual_account_skipped_and_reported(self, broadcasts):
|
||||
conn = _Conn({})
|
||||
out = scan_engine._resolve_manual_users(conn, [
|
||||
{"id": "guid-1", "displayName": "One"},
|
||||
{"id": "manual:ghost@school.dk", "displayName": "ghost"},
|
||||
])
|
||||
assert out == [{"id": "guid-1", "displayName": "One"}]
|
||||
assert broadcasts and broadcasts[0][0] == "scan_error"
|
||||
assert "ghost@school.dk" in broadcasts[0][1]["file"]
|
||||
|
||||
def test_manual_account_already_selected_is_deduplicated(self, broadcasts):
|
||||
conn = _Conn({"ann@school.dk": {"id": "guid-ann", "displayName": "Ann"}})
|
||||
out = scan_engine._resolve_manual_users(conn, [
|
||||
{"id": "guid-ann", "displayName": "Ann"},
|
||||
{"id": "manual:ann@school.dk", "displayName": "ann"},
|
||||
])
|
||||
assert [u["id"] for u in out] == ["guid-ann"]
|
||||
|
||||
def test_all_manual_unresolvable_gives_empty_list_not_all_users(self, broadcasts):
|
||||
# run_scan resolves inside the non-empty branch, so an empty result must
|
||||
# stay empty — it must never widen to "scan every tenant user".
|
||||
conn = _Conn({})
|
||||
assert scan_engine._resolve_manual_users(
|
||||
conn, [{"id": "manual:x@school.dk", "displayName": "x"}]) == []
|
||||
|
||||
|
||||
class TestLookupRoute:
|
||||
@pytest.fixture()
|
||||
def client(self):
|
||||
import gdpr_scanner
|
||||
gdpr_scanner.app.config["TESTING"] = True
|
||||
with gdpr_scanner.app.test_client() as c:
|
||||
yield c
|
||||
|
||||
def test_lookup_uses_connector(self, client, monkeypatch):
|
||||
from routes import state
|
||||
conn = MagicMock()
|
||||
conn.lookup_user.return_value = {"id": "guid-ann", "displayName": "Ann",
|
||||
"mail": "ann@school.dk"}
|
||||
monkeypatch.setattr(state, "connector", conn)
|
||||
r = client.get("/api/users/lookup?upn=ann@school.dk")
|
||||
assert r.status_code == 200
|
||||
assert r.get_json()["id"] == "guid-ann"
|
||||
conn.lookup_user.assert_called_once_with("ann@school.dk")
|
||||
|
||||
def test_lookup_not_found_is_404(self, client, monkeypatch):
|
||||
from routes import state
|
||||
conn = MagicMock()
|
||||
conn.lookup_user.side_effect = RuntimeError("Request_ResourceNotFound")
|
||||
monkeypatch.setattr(state, "connector", conn)
|
||||
assert client.get("/api/users/lookup?upn=ghost@school.dk").status_code == 404
|
||||
Reference in new issue
Block a user