mirror of
https://github.com/okieselbach/AutopilotMonitor.git
synced 2026-10-24 12:42:38 +00:00
fix(backend): managed tenants see operator-provisioned access as "Platform support"
GET delegations/managers no longer exposes the name of an operator-created Tenant Group to the customer; both operator shapes (groups and direct grants) carry the neutral label. Assignees, reader count and source stay visible. Web badge reads "Provisioned by operators". D-267. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
This commit is contained in:
+1
-1
Submodule internal updated: b54f43f711...4c5963c82c
@@ -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<string> { operatorGroupId });
|
||||
h.Repo.Setup(x => x.GetTenantGroupAsync(operatorGroupId)).ReturnsAsync(new TenantGroup
|
||||
{
|
||||
GroupId = operatorGroupId,
|
||||
Name = "Internal MSP customers north",
|
||||
OwnerTenantId = null,
|
||||
TenantIds = new List<string> { Customer, Other },
|
||||
Assignees = new List<TenantGroupAssignment>
|
||||
{
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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<TenantManagerAssignee> { 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<TenantManagerAssignee> { new() { Upn = "analyst@partner.example", Role = "DelegatedReader", IsEnabled = true } }, Revocable = false },
|
||||
},
|
||||
});
|
||||
|
||||
@@ -47,6 +47,11 @@ public class DelegationSelfService
|
||||
{
|
||||
public const string SourceSelfService = "self-service";
|
||||
public const string SourceOperator = "operator";
|
||||
/// <summary>
|
||||
/// 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.
|
||||
/// </summary>
|
||||
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;
|
||||
}
|
||||
|
||||
@@ -163,6 +163,7 @@ namespace AutopilotMonitor.Shared.Models
|
||||
/// <summary>The managing tenant that owns the group (self-service); absent for operator-created groups and direct grants.</summary>
|
||||
public string? OwnerTenantId { get; set; }
|
||||
public string? OwnerDomain { get; set; }
|
||||
/// <summary>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.</summary>
|
||||
public string Name { get; set; } = default!;
|
||||
/// <summary>self-service | operator</summary>
|
||||
public string Source { get; set; } = default!;
|
||||
|
||||
+1
-1
@@ -209,7 +209,7 @@ export function SectionDelegatedAccess() {
|
||||
<li key={key} className="py-2 flex flex-wrap items-center gap-x-3 gap-y-1 text-sm">
|
||||
<span className="font-medium text-gray-900">{m.ownerDomain || m.name}</span>
|
||||
<span className={`inline-flex items-center px-1.5 py-0.5 rounded text-xs font-medium ${m.source === "self-service" ? "bg-sky-100 text-sky-800" : "bg-gray-100 text-gray-700"}`}>
|
||||
{m.source === "self-service" ? "Delegated by you" : "Platform operators"}
|
||||
{m.source === "self-service" ? "Delegated by you" : "Provisioned by operators"}
|
||||
</span>
|
||||
<span className="text-xs text-gray-500">
|
||||
{m.assignees.length} reader{m.assignees.length === 1 ? "" : "s"}
|
||||
|
||||
Reference in New Issue
Block a user