14 KiB
Feature Landscape
Domain: MSP IT admin desktop tool — SharePoint audit report branding + user directory browse Milestone: v2.2 — Report Branding & User Directory Researched: 2026-04-08 Overall confidence: HIGH (verified via official Graph API docs + direct codebase inspection)
Scope Boundary
This file covers only the two net-new features in v2.2:
- HTML report branding (MSP logo + client logo per tenant)
- User directory browse mode in the user access audit tab
Everything else is already shipped. Dependencies on existing code are called out explicitly.
Feature 1: HTML Report Branding
Table Stakes
Features an MSP admin expects without being asked. If missing, the reports feel unfinished and unprofessional to hand to a client.
| Feature | Why Expected | Complexity | Notes |
|---|---|---|---|
| MSP global logo in report header | Every white-label MSP tool shows the MSP's own brand on deliverables | Low | Single image stored in AppSettings or a dedicated branding settings section |
| Client (per-tenant) logo in report header | MSP reports are client-facing; client should see their own logo next to the MSP's | Medium | Stored in TenantProfile; 2 sources: import from file or pull from tenant |
| Logo renders in self-contained HTML (no external URL) | Reports are often emailed or archived; external URLs break offline | Low | Base64-encode and embed as data:image/...;base64,... inline in <img src= |
| Logo graceful absence (no logo configured = no broken image) | Admins will run the tool before configuring logos | Trivial | Conditional render — omit the <img> block entirely when no logo is set |
| Consistent placement across all HTML export types | App already ships 5+ HTML exporters; logos must appear in all of them | Medium | Extract a shared header-builder method or inject a branding context into each export service |
Differentiators
Features not expected by default, but add meaningful value once table stakes are covered.
| Feature | Value Proposition | Complexity | Notes |
|---|---|---|---|
| Auto-pull client logo from Microsoft Entra tenant branding | Zero-config for tenants that already have a banner logo set in Entra ID | Medium | Graph API: GET /organization/{id}/branding/localizations/default/bannerLogo returns raw image bytes. Least-privileged scope is User.Read (delegated, already in use). Returns empty body or 404 when not configured — must handle gracefully. |
| Report timestamp and tenant display name in header | Contextualizes archived reports without needing to inspect the filename | Low | TenantProfile.TenantUrl already available; display name derivable from domain |
Anti-Features
Do not build these. They add scope without proportionate MSP value.
| Anti-Feature | Why Avoid | What to Do Instead |
|---|---|---|
| Color theme / CSS customization per tenant | Complexity explodes — per-tenant CSS is a design system problem, not an admin tool feature | Stick to a single professional neutral theme; logo is sufficient branding |
| PDF export with embedded logo | PDF generation requires a third-party library (iTextSharp, QuestPDF, etc.) adding binary size to the 200 MB EXE | Document in release notes that users can print-to-PDF from browser |
| Animated or SVG logo support | MIME handling complexity; SVG in data-URIs introduces XSS risk | Support PNG/JPG/GIF only; reject SVG at import time |
| Logo URL field (hotlinked) | Reports break when URL becomes unavailable; creates external dependency for a local-first tool | Force file import with base64 embedding |
Feature Dependencies
AppSettings + MspLogoBase64 (string?, nullable)
TenantProfile + ClientLogoBase64 (string?, nullable)
+ ClientLogoSource (enum: None | Imported | AutoPulled)
Shared branding helper → called by HtmlExportService, UserAccessHtmlExportService,
StorageHtmlExportService, DuplicatesHtmlExportService,
SearchHtmlExportService
Auto-pull code path → Graph API call via existing GraphClientFactory
Logo import UI → WPF OpenFileDialog -> File.ReadAllBytes -> Convert.ToBase64String
-> stored in profile JSON via existing ProfileRepository
Key existing code note: All 5+ HTML export services currently build their <body> independently
with no shared header. Branding requires one of:
- (a) a
ReportBrandingContextrecord passed into each exporter'sBuildHtmlmethod, or - (b) a
HtmlReportHeaderBuilderstatic/injectable helper all exporters call.
Option (b) is lower risk — it does not change method signatures that existing unit tests already call.
Complexity Assessment
| Sub-task | Complexity | Reason |
|---|---|---|
| AppSettings + TenantProfile model field additions | Low | Trivial nullable-string fields; JSON serialization already in place |
| Settings UI: MSP logo upload + preview | Low | WPF OpenFileDialog + BitmapImage from base64, standard pattern |
| ProfileManagementDialog: client logo upload per tenant | Low | Same pattern as MSP logo |
| Shared HTML header builder with logo injection | Low-Medium | One helper; replaces duplicated header HTML in 5 exporters |
Auto-pull from Entra bannerLogo endpoint |
Medium | Async Graph call; must handle 404, empty stream, no branding configured |
| Localization keys EN/FR for new labels | Low | ~6-10 new keys; 220+ already managed |
Feature 2: User Directory Browse Mode
Table Stakes
Features an admin expects when a "browse all users" mode is offered alongside the existing search.
| Feature | Why Expected | Complexity | Notes |
|---|---|---|---|
| Full directory listing (all member users, paginated) | Browse implies seeing everyone, not just name-search hits | Medium | Graph GET /users with $top=100, follow @odata.nextLink until null. Max page size is 999 but 100 pages give better progress feedback |
| Searchable/filterable within the loaded list | Once loaded, admins filter locally without re-querying | Low | In-memory filter on DisplayName, UPN, Mail — same pattern used in PermissionsView DataGrid |
| Sortable columns (Name, UPN) | Standard expectation for any directory table | Low | WPF DataGrid column sorting, already used in other tabs |
| Select user from list to run access audit | The whole point — browse replaces the people-picker for users the admin cannot spell | Low | Bind selected item; reuse the existing IUserAccessAuditService pipeline unchanged |
| Loading indicator with progress count | Large tenants (5k+ users) take several seconds to page through | Low | Existing OperationProgress pattern; show "Loaded X users..." counter |
| Toggle between Browse mode and Search (people-picker) mode | Search is faster for known users; browse is for discovery | Low | RadioButton or ToggleButton in the tab toolbar; visibility-toggle two panels |
Differentiators
| Feature | Value Proposition | Complexity | Notes |
|---|---|---|---|
| Filter by account type (member vs guest) | MSPs care about guest proliferation; helps scope audit targets | Low | Graph returns userType field; add a toggle filter. Include in $select |
| Department / Job Title columns | Helps identify the right user in large tenants with common names | Low-Medium | Include department, jobTitle in $select; optional columns in DataGrid |
| Session-scoped directory cache | Avoids re-fetching full tenant list on every tab visit | Medium | Store list in ViewModel or session-scoped service; invalidate on TenantSwitchedMessage |
Anti-Features
| Anti-Feature | Why Avoid | What to Do Instead |
|---|---|---|
| Eager load on tab open | Large tenants (10k+ users) block UI and risk Graph throttling on every tab navigation | Lazy-load on explicit "Load Directory" button click; show a clear affordance |
| Delta query / incremental sync | Delta queries are for maintaining a local replica over time; wrong pattern for a one-time audit session | Single paginated GET per session; add a Refresh button |
| Multi-user bulk select for simultaneous audit | The audit pipeline is per-user by design; multi-user requires a fundamentally different results model | Out of scope; single-user selection only |
| Export the user directory to CSV | That is an identity reporting feature (AdminDroid et al.), not an access audit feature | Out of scope for this milestone |
| Show disabled accounts by default | Disabled users do not have active SharePoint access; pollutes the list for audit purposes | Default $filter=accountEnabled eq true; optionally expose a toggle |
Feature Dependencies
New IGraphDirectoryService + GraphDirectoryService
→ GET /users?$select=displayName,userPrincipalName,mail,jobTitle,department,userType
&$filter=accountEnabled eq true
&$top=100
→ Follow @odata.nextLink in a loop until null
→ Uses existing GraphClientFactory (DI, unchanged)
UserAccessAuditViewModel additions:
+ IsBrowseMode (bool property, toggle)
+ DirectoryUsers (ObservableCollection<GraphUserResult> or new DirectoryUserEntry model)
+ DirectoryFilterText (string, filters in-memory)
+ LoadDirectoryCommand (async, cancellable)
+ IsDirectoryLoading (bool)
+ SelectedDirectoryUser → feeds into existing audit execution path
TenantSwitchedMessage handler in ViewModel: clear DirectoryUsers, reset IsBrowseMode
UserAccessAuditView.xaml:
+ Toolbar toggle (Search | Browse)
+ Visibility-collapsed people-picker panel when in browse mode
+ New DataGrid panel for browse mode
Key existing code note: GraphUserSearchService does filtered search only (startsWith filter +
ConsistencyLevel: eventual). Directory listing is a different call pattern — no filter, plain
pagination without ConsistencyLevel. A separate GraphDirectoryService is cleaner than extending
the existing service; search and browse have different cancellation and retry needs.
Complexity Assessment
| Sub-task | Complexity | Reason |
|---|---|---|
| IGraphDirectoryService + GraphDirectoryService (pagination loop) | Low-Medium | Standard Graph paging; same GraphClientFactory in DI |
| ViewModel additions (browse toggle, load command, filter, loading state) | Medium | New async command with progress, cancellation on tenant switch |
| View XAML: toggle + browse DataGrid panel | Medium | Visibility-toggle two panels; DataGrid column definitions |
| In-memory filter + column sort | Low | DataGrid pattern already used in PermissionsView |
| Loading indicator integration | Low | OperationProgress + IsLoading used by every tab |
| Localization keys EN/FR | Low | ~8-12 new keys |
| Unit tests for GraphDirectoryService | Low | Same mock pattern as GraphUserSearchService tests |
| Unit tests for ViewModel browse mode | Medium | Async load command, pagination mock, filter behavior |
Cross-Feature Dependencies
Both features touch the same data models. Changes must be coordinated:
TenantProfile model — gains fields for branding (ClientLogoBase64, ClientLogoSource)
AppSettings model — gains MspLogoBase64
ProfileRepository — serializes/deserializes new TenantProfile fields (JSON, backward-compat)
SettingsRepository — serializes/deserializes new AppSettings field
GraphClientFactory — used by both features (no changes needed)
TenantSwitchedMessage — consumed by UserAccessAuditViewModel to clear directory cache
Neither feature requires new NuGet packages. The Graph SDK, MSAL, and System.Text.Json are already present. No new binary dependencies means no EXE size increase.
MVP Recommendation
Build in this order, each independently releasable:
- MSP logo in HTML reports — highest visible impact, lowest complexity. AppSettings field + Settings UI upload + shared header builder.
- Client logo in HTML reports (import from file) — completes the co-branding pattern. TenantProfile field + ProfileManagementDialog upload UI.
- User directory browse (load + select + filter) — core browse UX. Toggle, paginated load, in-memory filter, pipe into existing audit.
- Auto-pull client logo from Entra branding — differentiator, zero-config polish. Build after manual import works so the fallback path is proven.
- Directory: guest filter + department/jobTitle columns — low-effort differentiators; add after core browse is stable.
Defer to a later milestone:
- Directory session caching across tab switches — a Refresh button is sufficient for v2.2.
- Logo on CSV exports — CSV has no image support; not applicable.
Sources
- Graph API List Users (v1.0 official): https://learn.microsoft.com/en-us/graph/api/user-list?view=graph-rest-1.0 — HIGH confidence
- Graph API Get organizationalBranding (v1.0 official): https://learn.microsoft.com/en-us/graph/api/organizationalbranding-get?view=graph-rest-1.0 — HIGH confidence
- Graph API bannerLogo stream:
GET /organization/{id}/branding/localizations/default/bannerLogo— HIGH confidence (verified in official docs) - Graph pagination concepts: https://learn.microsoft.com/en-us/graph/paging — HIGH confidence
- ControlMap co-branding (MSP + client logo pattern): https://help.controlmap.io/hc/en-us/articles/24174398424347 — MEDIUM confidence
- ManageEngine ServiceDesk Plus MSP per-account branding: https://www.manageengine.com/products/service-desk-msp/rebrand.html — MEDIUM confidence
- SolarWinds MSP report customization: http://allthings.solarwindsmsp.com/2013/06/customize-your-branding-on-client.html — MEDIUM confidence
- Direct codebase inspection: HtmlExportService.cs, GraphUserSearchService.cs, AppSettings.cs, TenantProfile.cs — HIGH confidence