Files
AutopilotMonitor/src/Shared/AutopilotMonitor.Shared/DataAccess/IAdminRepository.cs
T
Oliver Kieselbach 05a591e1d4 feat(backend): delegated-admin (MSP mode) data model + scope service [Phase 0]
Foundation for "MSP mode" — a scoped-global authorization tier between a
single-tenant member and a platform GlobalAdmin. A delegated admin may access a
SUBSET of tenants (its assignment rows), resolved UPN-based and tid-agnostic,
exactly like GlobalAdmins.

Phase 0 is purely additive: the service is registered but no auth path consults
it yet (the cross-tenant gate change lands in Phase 1), so behavior is unchanged.

- Constants: DelegatedRoles/DelegatedStatus/DelegatedSource; new DelegatedAdmins
  table added to TableNames.All (auto-create), CriticalBackupTables.All and
  AuthTablesFullRestoreForbidden (identity table — full-table restore blocked).
- IAdminRepository + TableAdminRepository: DelegatedAdminEntry DTO + CRUD
  (PK=UPN, RK=TenantId). Hot-path GetDelegatedTenantsAsync (partition scan) and
  reverse GetDelegatedAssigneesAsync. Queries use the typed predicate overload
  (no string-interpolated OData filter — avoids filter injection).
- DelegatedAdminService: resolves DelegatedScope (Covers/RoleFor/CanWrite) with a
  5-min cache + write-through invalidation. Fail-closed: only Active+enabled rows
  with a recognized role confer scope; empty role -> DelegatedReader; unknown -> dropped.
- 16 unit tests (fake repo); full Functions suite 2434/2434 green. Backup
  catalog-count assertion bumped 15 -> 16.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-25 18:50:56 +02:00

109 lines
5.6 KiB
C#

using System;
using System.Collections.Generic;
using System.Threading.Tasks;
namespace AutopilotMonitor.Shared.DataAccess
{
/// <summary>
/// Repository for admin and tenant member management.
/// Covers: GlobalAdmins, TenantAdmins, McpUsers, DelegatedAdmins tables.
/// </summary>
public interface IAdminRepository
{
// --- Global Admins ---
Task<bool> IsGlobalAdminAsync(string upn);
/// <summary>
/// Resolves the platform role for a UPN: "GlobalAdmin", "GlobalReader", or null when there is no
/// enabled GlobalAdmins row. Empty/missing Role on an existing enabled row ⇒ "GlobalAdmin" (back-compat).
/// </summary>
Task<string?> GetGlobalRoleAsync(string upn);
Task<List<GlobalAdminEntry>> GetAllGlobalAdminsAsync();
Task<bool> AddGlobalAdminAsync(string upn, string addedBy);
Task<bool> RemoveGlobalAdminAsync(string upn);
Task<bool> DisableGlobalAdminAsync(string upn);
// --- MCP Users ---
Task<bool> IsMcpUserAsync(string upn);
Task<McpUserEntry?> GetMcpUserAsync(string upn);
Task<List<McpUserEntry>> GetAllMcpUsersAsync();
Task<bool> AddMcpUserAsync(string upn, string addedBy);
Task<bool> RemoveMcpUserAsync(string upn);
Task<bool> SetMcpUserEnabledAsync(string upn, bool isEnabled);
Task<bool> SetMcpUserUsagePlanAsync(string upn, string? usagePlan);
// --- Delegated Admins (scoped-global / "MSP mode") ---
/// <summary>All assignment rows for one delegated-admin UPN (PK scan), regardless of status/enabled.
/// Callers that need only effective scope filter on Status==Active &amp;&amp; IsEnabled.</summary>
Task<List<DelegatedAdminEntry>> GetDelegatedTenantsAsync(string upn);
/// <summary>All assignment rows targeting one tenant (cross-partition RowKey scan — admin UI, not hot path).
/// Powers the customer's "who manages my tenant?" view including pending requests.</summary>
Task<List<DelegatedAdminEntry>> GetDelegatedAssigneesAsync(string tenantId);
Task<DelegatedAdminEntry?> GetDelegatedAdminAsync(string upn, string tenantId);
/// <summary>Creates or replaces an assignment row (upsert on PK=upn, RK=tenantId).</summary>
Task<bool> UpsertDelegatedAdminAsync(string upn, string tenantId, string role, string status, string source, string grantedBy);
Task<bool> SetDelegatedAdminStatusAsync(string upn, string tenantId, string status);
Task<bool> SetDelegatedAdminEnabledAsync(string upn, string tenantId, bool isEnabled);
Task<bool> RemoveDelegatedAdminAsync(string upn, string tenantId);
// --- Tenant Members ---
Task<List<TenantMember>> GetTenantMembersAsync(string tenantId);
Task<bool> AddTenantMemberAsync(string tenantId, string upn, string addedBy, string role, bool canManageBootstrapTokens = false);
Task<bool> RemoveTenantMemberAsync(string tenantId, string upn);
Task<bool> UpdateMemberPermissionsAsync(string tenantId, string upn, string role, bool canManageBootstrapTokens);
Task<bool> SetTenantMemberEnabledAsync(string tenantId, string upn, bool isEnabled);
Task<TenantMember?> GetTenantMemberAsync(string tenantId, string upn);
Task<bool> IsTenantAdminAsync(string tenantId, string upn);
Task<bool> IsTenantMemberAsync(string tenantId, string upn);
}
public class GlobalAdminEntry
{
public string Upn { get; set; } = string.Empty;
public bool IsEnabled { get; set; } = true;
public DateTime AddedAt { get; set; }
public string AddedBy { get; set; } = string.Empty;
/// <summary>Platform role: "GlobalAdmin" (default) or "GlobalReader". Empty ⇒ GlobalAdmin (back-compat).</summary>
public string Role { get; set; } = string.Empty;
}
/// <summary>
/// One delegated-admin assignment: UPN X may access tenant Y at role Role. The "scoped global" tier
/// (subset of tenants) between a single-tenant member and a platform GlobalAdmin. Surfaced as "MSP mode".
/// </summary>
public class DelegatedAdminEntry
{
public string Upn { get; set; } = string.Empty;
public string TenantId { get; set; } = string.Empty;
/// <summary>Constants.DelegatedRoles: "DelegatedReader" (default) or "DelegatedAdmin".</summary>
public string Role { get; set; } = string.Empty;
public bool IsEnabled { get; set; } = true;
/// <summary>Constants.DelegatedStatus: "Active" / "PendingApproval" / "Revoked". Only Active confers scope.</summary>
public string Status { get; set; } = string.Empty;
/// <summary>Constants.DelegatedSource: "OperatorGranted" / "CustomerDelegated".</summary>
public string Source { get; set; } = string.Empty;
public DateTime GrantedAt { get; set; }
public string GrantedBy { get; set; } = string.Empty;
}
public class McpUserEntry
{
public string Upn { get; set; } = string.Empty;
public bool IsEnabled { get; set; } = true;
public DateTime AddedAt { get; set; }
public string AddedBy { get; set; } = string.Empty;
public string? UsagePlan { get; set; }
}
public class TenantMember
{
public string Upn { get; set; } = string.Empty;
public string TenantId { get; set; } = string.Empty;
public string Role { get; set; } = "Admin";
public bool IsEnabled { get; set; } = true;
public bool CanManageBootstrapTokens { get; set; }
public DateTime AddedAt { get; set; }
public string AddedBy { get; set; } = string.Empty;
}
}