diff --git a/internal b/internal index b54f43f7..4c5963c8 160000 --- a/internal +++ b/internal @@ -1 +1 @@ -Subproject commit b54f43f711430067944c828b7d27a7a6dc6a4f59 +Subproject commit 4c5963c82c81c08c4487c45ba7812ea8af941c60 diff --git a/src/Backend/AutopilotMonitor.Functions.Tests/DelegationSelfServiceTests.cs b/src/Backend/AutopilotMonitor.Functions.Tests/DelegationSelfServiceTests.cs index 642d6c1a..99165065 100644 --- a/src/Backend/AutopilotMonitor.Functions.Tests/DelegationSelfServiceTests.cs +++ b/src/Backend/AutopilotMonitor.Functions.Tests/DelegationSelfServiceTests.cs @@ -526,7 +526,41 @@ public class DelegationSelfServiceTests Assert.Equal(Now.AddDays(-3), owned.SinceUtc); var direct = managers.Single(m => m.GroupId == null); Assert.Equal(DelegationSelfService.SourceOperator, direct.Source); + Assert.Equal(DelegationSelfService.OperatorLabel, direct.Name); Assert.False(direct.Revocable); Assert.Single(direct.Assignees); // the revoked row confers nothing } + + [Fact] + public async Task ListManagers_OperatorGroup_ShowsNeutralLabelNotItsName_ButItsAssignees() + { + const string operatorGroupId = "0f1e2d3c4b5a69788796a5b4c3d2e1f0"; + var h = Build(); + h.Repo.Setup(x => x.GetGroupIdsContainingTenantAsync(Customer)).ReturnsAsync(new List { operatorGroupId }); + h.Repo.Setup(x => x.GetTenantGroupAsync(operatorGroupId)).ReturnsAsync(new TenantGroup + { + GroupId = operatorGroupId, + Name = "Internal MSP customers north", + OwnerTenantId = null, + TenantIds = new List { Customer, Other }, + Assignees = new List + { + new() { Upn = "ops@vendor.example", GroupId = operatorGroupId, Role = Constants.DelegatedRoles.DelegatedReader, IsEnabled = true }, + new() { Upn = "ops2@vendor.example", GroupId = operatorGroupId, Role = Constants.DelegatedRoles.DelegatedReader, IsEnabled = true }, + }, + }); + + var managers = await h.Svc.ListManagersAsync(Customer); + + var group = Assert.Single(managers); + Assert.Equal(operatorGroupId, group.GroupId); + Assert.Equal(DelegationSelfService.OperatorLabel, group.Name); + Assert.DoesNotContain("north", group.Name, StringComparison.OrdinalIgnoreCase); + Assert.Null(group.OwnerTenantId); + Assert.Null(group.OwnerDomain); + Assert.Null(group.SinceUtc); + Assert.Equal(DelegationSelfService.SourceOperator, group.Source); + Assert.False(group.Revocable); + Assert.Equal(2, group.Assignees.Count); + } } diff --git a/src/Backend/AutopilotMonitor.Functions.Tests/DelegationWireParityTests.cs b/src/Backend/AutopilotMonitor.Functions.Tests/DelegationWireParityTests.cs index 09f3a8f2..fe3636d4 100644 --- a/src/Backend/AutopilotMonitor.Functions.Tests/DelegationWireParityTests.cs +++ b/src/Backend/AutopilotMonitor.Functions.Tests/DelegationWireParityTests.cs @@ -194,7 +194,7 @@ public class DelegationWireParityTests managers = new object[] { new { groupId = (string?)("msp-" + Home), ownerTenantId = (string?)Home, ownerDomain = (string?)"partner.example", name = "Customers of partner.example", source = "self-service", assignees, sinceUtc = (DateTime?)since, revocable = true }, - new { groupId = nullGroup, ownerTenantId = nullOwner, ownerDomain = nullDomain, name = "Platform operators", source = "operator", assignees, sinceUtc = (DateTime?)null, revocable = false }, + new { groupId = nullGroup, ownerTenantId = nullOwner, ownerDomain = nullDomain, name = "Platform support", source = "operator", assignees, sinceUtc = (DateTime?)null, revocable = false }, }, }, new TenantManagerListResponse @@ -204,7 +204,7 @@ public class DelegationWireParityTests { new() { GroupId = "msp-" + Home, OwnerTenantId = Home, OwnerDomain = "partner.example", Name = "Customers of partner.example", Source = "self-service", Assignees = new List { new() { Upn = "analyst@partner.example", Role = "DelegatedReader", IsEnabled = true } }, SinceUtc = since, Revocable = true }, - new() { Name = "Platform operators", Source = "operator", + new() { Name = "Platform support", Source = "operator", Assignees = new List { new() { Upn = "analyst@partner.example", Role = "DelegatedReader", IsEnabled = true } }, Revocable = false }, }, }); diff --git a/src/Backend/AutopilotMonitor.Functions/Services/DelegationSelfService.cs b/src/Backend/AutopilotMonitor.Functions/Services/DelegationSelfService.cs index e67ed5b6..83b32e00 100644 --- a/src/Backend/AutopilotMonitor.Functions/Services/DelegationSelfService.cs +++ b/src/Backend/AutopilotMonitor.Functions/Services/DelegationSelfService.cs @@ -47,6 +47,11 @@ public class DelegationSelfService { public const string SourceSelfService = "self-service"; public const string SourceOperator = "operator"; + /// + /// The one label a managed tenant sees for every operator-provisioned access (operator-created groups and + /// direct grants alike). Operator groups are internal organisation — their names never reach a customer. + /// + public const string OperatorLabel = "Platform support"; private const string AuditGroupAccess = "DelegatedGroupAccess"; private const string AuditInvitation = "DelegationInvitation"; private const string AuditManagedTenant = "DelegationManagedTenant"; @@ -377,8 +382,11 @@ public class DelegationSelfService .Select(r => r.AcceptedAt) .Max(); } - result.Add(new TenantManagerView(group.GroupId, owner, owner != null ? await DomainAsync(owner) : null, - group.Name, owner != null ? SourceSelfService : SourceOperator, group.Assignees, since, Revocable: owner != null)); + // An owned group carries its managing tenant's name; an operator group is internal and shows + // only the neutral label (its assignees stay visible — the customer must know who can read it). + result.Add(owner != null + ? new TenantManagerView(group.GroupId, owner, await DomainAsync(owner), group.Name, SourceSelfService, group.Assignees, since, Revocable: true) + : new TenantManagerView(group.GroupId, null, null, OperatorLabel, SourceOperator, group.Assignees, null, Revocable: false)); } var direct = (await _adminRepo.GetDelegatedAssigneesAsync(target)) @@ -386,7 +394,7 @@ public class DelegationSelfService .Select(r => new TenantGroupAssignment { Upn = r.Upn, GroupId = string.Empty, Role = r.Role, IsEnabled = r.IsEnabled, AssignedBy = r.GrantedBy, AssignedAt = r.GrantedAt }) .ToList(); if (direct.Count > 0) - result.Add(new TenantManagerView(null, null, null, "Platform operators", SourceOperator, direct, direct.Min(d => d.AssignedAt), Revocable: false)); + result.Add(new TenantManagerView(null, null, null, OperatorLabel, SourceOperator, direct, direct.Min(d => d.AssignedAt), Revocable: false)); return result; } diff --git a/src/Shared/AutopilotMonitor.Shared/Models/DelegationApiModels.cs b/src/Shared/AutopilotMonitor.Shared/Models/DelegationApiModels.cs index 62532997..aabfdd92 100644 --- a/src/Shared/AutopilotMonitor.Shared/Models/DelegationApiModels.cs +++ b/src/Shared/AutopilotMonitor.Shared/Models/DelegationApiModels.cs @@ -163,6 +163,7 @@ namespace AutopilotMonitor.Shared.Models /// The managing tenant that owns the group (self-service); absent for operator-created groups and direct grants. public string? OwnerTenantId { get; set; } public string? OwnerDomain { get; set; } + /// The owned group's name for a self-service delegation; the neutral operator label ("Platform support") for every operator-provisioned entry — internal group names never reach a customer. public string Name { get; set; } = default!; /// self-service | operator public string Source { get; set; } = default!; diff --git a/src/Web/autopilot-monitor-web/app/settings/tenant/sections/SectionDelegatedAccess.tsx b/src/Web/autopilot-monitor-web/app/settings/tenant/sections/SectionDelegatedAccess.tsx index 8cce42e7..2a0a4984 100644 --- a/src/Web/autopilot-monitor-web/app/settings/tenant/sections/SectionDelegatedAccess.tsx +++ b/src/Web/autopilot-monitor-web/app/settings/tenant/sections/SectionDelegatedAccess.tsx @@ -209,7 +209,7 @@ export function SectionDelegatedAccess() {
  • {m.ownerDomain || m.name} - {m.source === "self-service" ? "Delegated by you" : "Platform operators"} + {m.source === "self-service" ? "Delegated by you" : "Provisioned by operators"} {m.assignees.length} reader{m.assignees.length === 1 ? "" : "s"}