Operators previously saw editable settings forms bound to hardcoded defaults
whose Save always 403'd. Now they see the tenant's real configuration
read-only, with no save affordances at all.
Backend:
- GET config/{tenantId}: TenantAdminOrGlobalReader -> MemberRead. Safe because
the handler redacts by default (CanViewSecrets: only GA or own-tenant Admin
see secrets); Operators/Viewers receive RedactedCopyForReader().
- POST diagnostics/files: TenantAdminOrGA -> TenantAdminOrOperator (submitting
logs is a troubleshooting action; Viewer and read-only GlobalReader excluded).
- Tests: policy pins updated; new middleware tests (own-tenant Operator allowed
+ redacted, cross-tenant Operator 403); CanViewSecrets theory for
Operator/Viewer.
Web:
- TenantConfigContext loads the full config for all settings users (403
fallback to feature-flags minimal config for deploy-order safety) and
exposes canEditConfig; saveConfiguration guards on it.
- New ReadOnlyFieldset wraps section content (native fieldset disable, values
stay copyable); SaveResetBar is not rendered at all for read-only viewers.
Applied to Contact, SLA Targets, Hardware Whitelist (+ rejection-insights
toggle), Notifications, Agent Settings, Agent Analyzers, Diagnostics, Data
Management.
- Hidden entirely from Operators (in-page admin-only card + sidebar sub-item
gate): Autopilot Validation, Access Management, Offboarding, Unrestricted
Mode. GlobalReader sidebar parity kept via hasGlobalScope.
Tests: backend 3257/3257, web vitest 627/627, tsc clean.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Wires the delegated ("scoped global" / MSP) tier into the auth middleware. A
delegated admin signs into their OWN home tenant but may now READ a SUBSET of
tenants — exactly the tenants in its DelegatedAdmins scope — without holding
full platform (GlobalAdmin/GlobalReader) scope.
Mechanism (PolicyEnforcementMiddleware.DecideAsync):
- Resolve the target tenant up-front (ResolveTarget), then resolve delegated
scope LAZILY — only when the caller lacks full platform scope AND the route is
a cross-tenant, tenant-scoped route. Keeps the common authenticated path free
of the extra (cached) table read. Service errors still fail closed to 503.
- A delegated grant applies ONLY to read tiers {MemberRead, TenantAdminOrGlobalReader}
on RouteParam/QueryParam routes, and ONLY for a target in the caller's scope.
It rescues a tier-denial for a delegated-only caller and lifts the cross-tenant
block for that target. Write tiers never admit a delegated caller (even a
DelegatedAdmin row grants read-only this phase), so no read->write escalation.
- Aggregate /api/global/* endpoints (GlobalReadOrAdmin) fan out over ALL tenants
and are deliberately NOT reachable by delegated callers until Phase 2 bounds
them to AllowedTenantIds.
RequestContext gains IsDelegatedReader/IsDelegatedAdmin/IsDelegated, HasFleetScope
and AllowedTenantIds (the delegated subset, for Phase 2 aggregation).
Secret-leak hardening: GetTenantConfigurationFunction now redacts by default —
only a Global Admin or the tenant's OWN admin sees unredacted secrets
(CanViewSecrets, extracted as a testable static). Behavior is identical for all
pre-existing roles; it closes the path where a cross-tenant delegated/global
reader would otherwise receive SAS/webhook secrets.
12 new tests (delegated allow/deny matrix + CanViewSecrets clearance). Full
Functions suite green aside from two pre-existing timing-flaky tests (unrelated
code, pass in isolation).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
GlobalReader = cross-tenant READ visibility like a Global Admin, but no mutating
operations and no platform settings. Provisioned exactly like a GA: a GlobalAdmins
table row with Role=GlobalReader (empty/legacy Role = GlobalAdmin, back-compat).
Semantics are ADDITIVE — a user who is also their own tenant's Admin keeps edit
rights on that tenant; a pure GlobalReader is read-only everywhere.
Backend:
- GlobalAdmins.Role column + GlobalAdminService.GetGlobalRoleAsync; IsGlobalAdminAsync
(incl. TableAdminRepository) delegates so a GlobalReader row never reads back as admin
- RequestContext.IsGlobalReader + HasGlobalScope
- Split GlobalAdminOnly into GlobalReadOrAdmin (cross-tenant reads) vs GlobalAdminOnly
(writes/settings) + TenantAdminOrGlobalReader for config-read
- PolicyEnforcementMiddleware: extracted testable DecideAsync seam; cross-tenant gate keys
off HasGlobalScope; additive own-tenant role carried so IsTenantAdmin stays correct
- Config secret redaction for readers (RedactedCopyForReader) + save-time placeholder
restore (RestoreRedactedSecretsFrom) so a redacted view can't blank real secrets
- raw table/log tools stay GlobalAdminOnly; SignalR global-admins read group admits HasGlobalScope
- /auth/me emits isGlobalReader (+ preview-gate bypass, no auto-admin); /auth/mcp emits globalRole
MCP: hasGlobalScope (GA or Reader) drives routing + tool catalog; raw secret-bearing tools
(query_table/list_tables/query_backend_logs) gated strict-GA only.
Web: AuthContext isGlobalReader + hasGlobalScope; reader reaches /dashboard + cross-tenant
views; platform Settings/Ops/Backups/Software GA-only (nav-hidden + requireGlobalAdmin route
layouts); mutating controls on mixed data pages disabled via useCanMutatePlatform; rules pages
read-only on foreign tenants; additive own-tenant edit preserved.
Tests: backend 2369, MCP 252, web 469 green.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>