mirror of
https://github.com/okieselbach/AutopilotMonitor.git
synced 2026-10-24 20:53:06 +00:00
feat(backend): delegated single-tenant access to /api/global/* reads (MSP switcher) [Phase 2a]
Lets a delegated ("MSP") admin read its managed tenants through the cross-tenant
/api/global/* endpoints — but ONLY on the single-tenant ?tenantId=X path, bounded
by the middleware to a tenant in its allowed set. This is what makes the tenant
SWITCHER work end-to-end (drill into a managed tenant, see its normal dashboard).
The dangerous no-tenantId AGGREGATE path (fan-out over ALL tenants) stays
GA/Reader-only and is untouched here; bounded fleet aggregation is a separate step.
- Middleware: GlobalReadOrAdmin added to the delegated read tier. The existing
`isScopedRoute && crossTenant` guard means a delegated grant still fires ONLY on
a tenant-scoped route for an in-scope target — so a delegated caller reaches a
global endpoint only via its single-tenant path, never the aggregate path.
- Catalog: the 27 GlobalReadOrAdmin routes whose handlers strictly restrict to the
named ?tenantId= (verified by an exhaustive per-endpoint audit) recataloged
None -> QueryParam. QueryParam/RouteParam on a GlobalReadOrAdmin route now
formally means "handler restricts to the named tenant => delegated-reachable";
aggregate-only routes (presence, platform metrics, distress-reports, config/all,
…) stay None and remain unreachable by delegated callers. Documented inline.
- Tests: delegated single-tenant global read allowed in-scope / denied out-of-scope
/ denied on the no-tenantId aggregate path; an aggregate-only route stays blocked
even with an in-scope ?tenantId=; Global Reader aggregate path regression; and a
catalog-contract theory pinning the SAFE(QueryParam) vs AGGREGATE-ONLY(None) split.
Updated GlobalAppsRoutes completeness assertion to QueryParam. Full suite green (2461).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
+6
-5
@@ -225,10 +225,11 @@ public class EndpointPolicyCatalogCompletenessTests
|
||||
|
||||
/// <summary>
|
||||
/// The three global/apps/* routes must be cross-tenant READS — GlobalReadOrAdmin (GA + read-only
|
||||
/// GlobalReader) — with TenantScoping.None (the optional ?tenantId= query param is intentional
|
||||
/// cross-tenant scoping, enforced inside the function — not by the middleware). A future accidental
|
||||
/// downgrade below the global-scope tier (e.g. to MemberRead, exposing them to tenant members)
|
||||
/// would be caught here, whereas the generic completeness test only guarantees *some* entry exists.
|
||||
/// GlobalReader). As of Phase 2a they carry TenantScoping.QueryParam: their handlers strictly restrict
|
||||
/// to the ?tenantId= named tenant, which is exactly what makes them reachable by a delegated ("MSP")
|
||||
/// admin bounded to its allowed tenant set (single-tenant path only; the no-tenantId aggregate path
|
||||
/// stays GA/Reader-only). A future accidental downgrade below the global-scope tier (e.g. to MemberRead,
|
||||
/// exposing them to tenant members) would be caught here.
|
||||
/// </summary>
|
||||
[Theory]
|
||||
[InlineData("GET", "/api/global/apps/list", "global/apps/list")]
|
||||
@@ -241,7 +242,7 @@ public class EndpointPolicyCatalogCompletenessTests
|
||||
Assert.NotNull(entry);
|
||||
Assert.Equal(expectedTemplate, entry!.RouteTemplate);
|
||||
Assert.Equal(EndpointPolicy.GlobalReadOrAdmin, entry.Policy);
|
||||
Assert.Equal(TenantScoping.None, entry.TenantScoping);
|
||||
Assert.Equal(TenantScoping.QueryParam, entry.TenantScoping);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
using System.Security.Claims;
|
||||
using AutopilotMonitor.Functions.Middleware;
|
||||
using AutopilotMonitor.Functions.Security;
|
||||
using AutopilotMonitor.Functions.Services;
|
||||
using AutopilotMonitor.Shared;
|
||||
using AutopilotMonitor.Shared.DataAccess;
|
||||
@@ -393,10 +394,11 @@ public class PolicyEnforcementMiddlewareTests
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Delegated_DoesNotReachAggregateGlobalEndpoint()
|
||||
public async Task Delegated_AggregateGlobalEndpoint_NoTenantId_IsForbidden()
|
||||
{
|
||||
// Cross-tenant AGGREGATE endpoints (GlobalReadOrAdmin) fan out over ALL tenants and are not
|
||||
// bounded to the delegated subset until Phase 2 — a delegated admin must NOT reach them yet.
|
||||
// The no-tenantId AGGREGATE path of a global endpoint fans out over ALL tenants. Even though
|
||||
// global/sessions is QueryParam-scoped (Phase 2a), a delegated caller WITHOUT ?tenantId= resolves no
|
||||
// target ⇒ no delegated grant ⇒ denied. Only the single-tenant (?tenantId=X) path is open to them.
|
||||
const string upn = "msp@partner.example";
|
||||
var h = BuildHarness();
|
||||
h.AsDelegated(TenantB);
|
||||
@@ -424,4 +426,93 @@ public class PolicyEnforcementMiddlewareTests
|
||||
// …but no config row was persisted for the MSP's home tenant A (nor any tenant).
|
||||
h.ConfigRepo.Verify(r => r.SaveTenantConfigurationAsync(It.IsAny<TenantConfiguration>()), Times.Never);
|
||||
}
|
||||
|
||||
// ── Phase 2a: delegated single-tenant access to cross-tenant /api/global/* read endpoints ──
|
||||
|
||||
[Fact]
|
||||
public async Task Delegated_GlobalEndpoint_SingleTenant_InScope_IsAllowed()
|
||||
{
|
||||
// global/sessions?tenantId=B — SAFE route (handler restricts to the named tenant), B in scope.
|
||||
const string upn = "msp@partner.example";
|
||||
var h = BuildHarness();
|
||||
h.AsDelegated(TenantB);
|
||||
|
||||
var result = await h.Middleware.DecideAsync("GET", "/api/global/sessions", TenantB, AuthedPrincipal(TenantA, upn));
|
||||
|
||||
Assert.True(result.Allowed);
|
||||
var rc = result.Context!;
|
||||
Assert.Equal(TenantB, rc.TargetTenantId);
|
||||
Assert.True(rc.IsDelegatedReader);
|
||||
Assert.False(rc.IsGlobalReader);
|
||||
Assert.Contains(TenantB.ToLowerInvariant(), rc.AllowedTenantIds!);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Delegated_GlobalEndpoint_SingleTenant_OutOfScope_IsForbidden()
|
||||
{
|
||||
// global/sessions?tenantId=C — C is not in the delegated set ⇒ blocked.
|
||||
const string upn = "msp@partner.example";
|
||||
var h = BuildHarness();
|
||||
h.AsDelegated(TenantB);
|
||||
|
||||
const string tenantC = "33333333-3333-3333-3333-333333333333";
|
||||
var result = await h.Middleware.DecideAsync("GET", "/api/global/sessions", tenantC, AuthedPrincipal(TenantA, upn));
|
||||
|
||||
Assert.False(result.Allowed);
|
||||
Assert.Equal(403, result.StatusCode);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Delegated_AggregateOnlyRoute_WithTenantId_StaysBlocked()
|
||||
{
|
||||
// global/presence is AGGREGATE-ONLY: its handler ignores ?tenantId= and returns ALL tenants'
|
||||
// presence. It MUST stay TenantScoping.None so a delegated caller can never reach it — even if they
|
||||
// pass a tenantId in their scope. (If this ever flips to QueryParam it's a cross-tenant leak.)
|
||||
const string upn = "msp@partner.example";
|
||||
var h = BuildHarness();
|
||||
h.AsDelegated(TenantB);
|
||||
|
||||
var result = await h.Middleware.DecideAsync("GET", "/api/global/presence", TenantB, AuthedPrincipal(TenantA, upn));
|
||||
|
||||
Assert.False(result.Allowed);
|
||||
Assert.Equal(403, result.StatusCode);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task GlobalReader_AggregateGlobalEndpoint_StillWorks()
|
||||
{
|
||||
// Regression: recataloging SAFE routes to QueryParam must not change full-platform behavior.
|
||||
// A Global Reader hitting the aggregate path (no tenantId) is still allowed (HasGlobalScope bypass).
|
||||
const string upn = "reader@contoso.com";
|
||||
var h = BuildHarness();
|
||||
h.AsGlobalRole(Constants.GlobalRoles.GlobalReader);
|
||||
|
||||
var result = await h.Middleware.DecideAsync("GET", "/api/global/sessions", null, AuthedPrincipal(TenantA, upn));
|
||||
|
||||
Assert.True(result.Allowed);
|
||||
}
|
||||
|
||||
// ── Catalog contract: which global routes are delegated-accessible (single-tenant) vs aggregate-only ──
|
||||
// QueryParam/RouteParam on a GlobalReadOrAdmin route == "handler restricts to the named tenant" ==
|
||||
// reachable by a delegated admin. None == aggregate-only == GA/Reader-only. Pins the leak-critical split.
|
||||
|
||||
[Theory]
|
||||
// SAFE — opened to delegated single-tenant access:
|
||||
[InlineData("GET", "/api/global/sessions", TenantScoping.QueryParam)]
|
||||
[InlineData("GET", "/api/global/audit/logs", TenantScoping.QueryParam)]
|
||||
[InlineData("GET", "/api/global/metrics/app", TenantScoping.QueryParam)]
|
||||
[InlineData("GET", "/api/global/metrics/fleet-health", TenantScoping.QueryParam)]
|
||||
[InlineData("GET", "/api/global/search/sessions", TenantScoping.QueryParam)]
|
||||
// AGGREGATE-ONLY — MUST stay None (unreachable by delegated):
|
||||
[InlineData("GET", "/api/global/presence", TenantScoping.None)]
|
||||
[InlineData("GET", "/api/global/metrics/platform", TenantScoping.None)]
|
||||
[InlineData("GET", "/api/global/distress-reports", TenantScoping.None)]
|
||||
[InlineData("GET", "/api/config/all", TenantScoping.None)]
|
||||
public void GlobalRoute_DelegatedAccessibility_MatchesContract(string method, string path, TenantScoping expected)
|
||||
{
|
||||
var entry = EndpointAccessPolicyCatalog.FindPolicy(method, path);
|
||||
Assert.NotNull(entry);
|
||||
Assert.Equal(EndpointPolicy.GlobalReadOrAdmin, entry!.Policy);
|
||||
Assert.Equal(expected, entry.TenantScoping);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -240,12 +240,21 @@ public class PolicyEnforcementMiddleware : IFunctionsWorkerMiddleware
|
||||
|
||||
/// <summary>
|
||||
/// The READ policy tiers a delegated (scoped-global / MSP) assignment may satisfy for a tenant in its
|
||||
/// scope. Deliberately excludes every write tier (TenantAdminOrGA, BootstrapManagerOrGA, GlobalAdminOnly)
|
||||
/// and the cross-tenant aggregate tier (GlobalReadOrAdmin — its endpoints fan out over ALL tenants and
|
||||
/// are only safe for delegated callers once bounded to AllowedTenantIds; that bounding lands in Phase 2).
|
||||
/// scope. Deliberately excludes every write tier (TenantAdminOrGA, BootstrapManagerOrGA, GlobalAdminOnly).
|
||||
///
|
||||
/// GlobalReadOrAdmin is included (Phase 2a) BUT a delegated grant only ever fires on a tenant-SCOPED
|
||||
/// route (RouteParam / QueryParam) for a target in the caller's allowed set — see the
|
||||
/// <c>isScopedRoute && crossTenant</c> guard in <see cref="DecideAsync"/>. A delegated caller therefore
|
||||
/// reaches a GlobalReadOrAdmin endpoint ONLY on its single-tenant (?tenantId=X / {tenantId}) path, never
|
||||
/// the no-tenantId AGGREGATE path (which fans out over ALL tenants). The catalog enforces the other half
|
||||
/// of this invariant: ONLY GlobalReadOrAdmin routes that strictly restrict to the named tenant carry
|
||||
/// QueryParam/RouteParam scoping; aggregate-only routes stay TenantScoping.None and are thus unreachable
|
||||
/// by delegated callers. Bounded cross-tenant aggregation for the fleet view is a separate later step.
|
||||
/// </summary>
|
||||
private static bool IsDelegatedReadTier(EndpointPolicy policy)
|
||||
=> policy is EndpointPolicy.MemberRead or EndpointPolicy.TenantAdminOrGlobalReader;
|
||||
=> policy is EndpointPolicy.MemberRead
|
||||
or EndpointPolicy.TenantAdminOrGlobalReader
|
||||
or EndpointPolicy.GlobalReadOrAdmin;
|
||||
|
||||
private async Task<CatalogDecisionResult> EvaluateCatalogPolicyAsync(
|
||||
ClaimsPrincipal? principal, EndpointPolicyEntry entry)
|
||||
|
||||
@@ -281,6 +281,15 @@ public static class EndpointAccessPolicyCatalog
|
||||
// reads but stay GlobalAdminOnly — they can dump secret-bearing tables and would bypass the
|
||||
// GlobalReader config-secret redaction.
|
||||
//
|
||||
// SECURITY — delegated ("MSP") accessibility (Phase 2a): a GlobalReadOrAdmin route carries
|
||||
// TenantScoping.QueryParam (or RouteParam) IFF its handler strictly restricts the response to the
|
||||
// single tenant named by ?tenantId= (or {tenantId}). That scoping is what lets a delegated admin
|
||||
// reach the route — bounded by the middleware to a tenant in its allowed set (single-tenant path
|
||||
// only; the no-tenantId AGGREGATE path stays GA/Reader-only). Routes that cannot restrict to one
|
||||
// tenant (they fan out over ALL tenants) MUST stay TenantScoping.None so delegated callers can
|
||||
// never reach them. Before adding QueryParam scoping to a global route, verify the handler honors
|
||||
// the filter — a wrong scoping here is a cross-tenant data leak.
|
||||
//
|
||||
// Inspector v1 endpoints (Plan §M6 — lift to MemberRead+QueryParam scoping at the v2 adminMode
|
||||
// lift). Cross-tenant resolution for global-scope callers happens inside the functions via
|
||||
// SessionsIndex, so TenantScoping.None on the catalog is fine.
|
||||
@@ -297,17 +306,17 @@ public static class EndpointAccessPolicyCatalog
|
||||
new("GET", "global/sessions/{sessionId}/deletion-manifest", EndpointPolicy.GlobalReadOrAdmin),
|
||||
new("GET", "global/tenants/{tenantId}/deletion-manifests", EndpointPolicy.GlobalReadOrAdmin, TenantScoping.RouteParam),
|
||||
new("GET", "global/session-deletions", EndpointPolicy.GlobalReadOrAdmin),
|
||||
new("GET", "global/raw/sessions", EndpointPolicy.GlobalReadOrAdmin),
|
||||
new("GET", "global/raw/events", EndpointPolicy.GlobalReadOrAdmin),
|
||||
new("GET", "global/raw/sessions", EndpointPolicy.GlobalReadOrAdmin, TenantScoping.QueryParam),
|
||||
new("GET", "global/raw/events", EndpointPolicy.GlobalReadOrAdmin, TenantScoping.QueryParam),
|
||||
// Raw table/log access stays GlobalAdminOnly: query_table can dump arbitrary tables
|
||||
// (incl. TenantConfiguration secrets), which would bypass the GlobalReader config-secret
|
||||
// redaction. A read-only GlobalReader uses the curated read endpoints instead.
|
||||
new("GET", "global/raw/tables", EndpointPolicy.GlobalAdminOnly),
|
||||
new("GET", "global/raw/tables/{tableName}", EndpointPolicy.GlobalAdminOnly),
|
||||
new("POST", "global/raw/logs", EndpointPolicy.GlobalAdminOnly),
|
||||
new("GET", "global/search/sessions", EndpointPolicy.GlobalReadOrAdmin),
|
||||
new("GET", "global/search/sessions-by-event", EndpointPolicy.GlobalReadOrAdmin),
|
||||
new("GET", "global/search/sessions-by-cve", EndpointPolicy.GlobalReadOrAdmin),
|
||||
new("GET", "global/search/sessions", EndpointPolicy.GlobalReadOrAdmin, TenantScoping.QueryParam),
|
||||
new("GET", "global/search/sessions-by-event", EndpointPolicy.GlobalReadOrAdmin, TenantScoping.QueryParam),
|
||||
new("GET", "global/search/sessions-by-cve", EndpointPolicy.GlobalReadOrAdmin, TenantScoping.QueryParam),
|
||||
new("GET", "global/metrics/summary", EndpointPolicy.GlobalReadOrAdmin),
|
||||
new("GET", "global/config", EndpointPolicy.GlobalReadOrAdmin),
|
||||
new("PUT", "global/config", EndpointPolicy.GlobalAdminOnly),
|
||||
@@ -321,31 +330,31 @@ public static class EndpointAccessPolicyCatalog
|
||||
new("DELETE", "preview/whitelist/{tenantId}", EndpointPolicy.GlobalAdminOnly, TenantScoping.RouteParam),
|
||||
new("GET", "preview/notification-email/{tenantId}", EndpointPolicy.GlobalReadOrAdmin, TenantScoping.RouteParam),
|
||||
new("POST", "preview/send-welcome-email/{tenantId}", EndpointPolicy.GlobalAdminOnly, TenantScoping.RouteParam),
|
||||
new("GET", "global/sessions", EndpointPolicy.GlobalReadOrAdmin),
|
||||
new("GET", "global/stats/sessions", EndpointPolicy.GlobalReadOrAdmin),
|
||||
new("GET", "global/audit/logs", EndpointPolicy.GlobalReadOrAdmin),
|
||||
new("GET", "global/sessions", EndpointPolicy.GlobalReadOrAdmin, TenantScoping.QueryParam),
|
||||
new("GET", "global/stats/sessions", EndpointPolicy.GlobalReadOrAdmin, TenantScoping.QueryParam),
|
||||
new("GET", "global/audit/logs", EndpointPolicy.GlobalReadOrAdmin, TenantScoping.QueryParam),
|
||||
new("GET", "global/presence", EndpointPolicy.GlobalReadOrAdmin),
|
||||
new("GET", "global/tenants-with-deletion-manifests", EndpointPolicy.GlobalReadOrAdmin),
|
||||
new("GET", "global/metrics/platform", EndpointPolicy.GlobalReadOrAdmin),
|
||||
new("GET", "global/metrics/app", EndpointPolicy.GlobalReadOrAdmin),
|
||||
new("GET", "global/metrics/fleet-health", EndpointPolicy.GlobalReadOrAdmin),
|
||||
new("GET", "global/apps/list", EndpointPolicy.GlobalReadOrAdmin),
|
||||
new("GET", "global/apps/{appName}/analytics", EndpointPolicy.GlobalReadOrAdmin),
|
||||
new("GET", "global/apps/{appName}/sessions", EndpointPolicy.GlobalReadOrAdmin),
|
||||
new("GET", "global/metrics/geographic", EndpointPolicy.GlobalReadOrAdmin),
|
||||
new("GET", "global/metrics/geographic/sessions", EndpointPolicy.GlobalReadOrAdmin),
|
||||
new("GET", "global/metrics/usage", EndpointPolicy.GlobalReadOrAdmin),
|
||||
new("GET", "global/metrics/sla", EndpointPolicy.GlobalReadOrAdmin),
|
||||
new("GET", "global/metrics/rule-stats", EndpointPolicy.GlobalReadOrAdmin),
|
||||
new("GET", "global/metrics/vulnerability", EndpointPolicy.GlobalReadOrAdmin),
|
||||
new("GET", "global/metrics/mcp-usage", EndpointPolicy.GlobalReadOrAdmin),
|
||||
new("GET", "global/metrics/mcp-usage/daily", EndpointPolicy.GlobalReadOrAdmin),
|
||||
new("GET", "global/metrics/app", EndpointPolicy.GlobalReadOrAdmin, TenantScoping.QueryParam),
|
||||
new("GET", "global/metrics/fleet-health", EndpointPolicy.GlobalReadOrAdmin, TenantScoping.QueryParam),
|
||||
new("GET", "global/apps/list", EndpointPolicy.GlobalReadOrAdmin, TenantScoping.QueryParam),
|
||||
new("GET", "global/apps/{appName}/analytics", EndpointPolicy.GlobalReadOrAdmin, TenantScoping.QueryParam),
|
||||
new("GET", "global/apps/{appName}/sessions", EndpointPolicy.GlobalReadOrAdmin, TenantScoping.QueryParam),
|
||||
new("GET", "global/metrics/geographic", EndpointPolicy.GlobalReadOrAdmin, TenantScoping.QueryParam),
|
||||
new("GET", "global/metrics/geographic/sessions", EndpointPolicy.GlobalReadOrAdmin, TenantScoping.QueryParam),
|
||||
new("GET", "global/metrics/usage", EndpointPolicy.GlobalReadOrAdmin, TenantScoping.QueryParam),
|
||||
new("GET", "global/metrics/sla", EndpointPolicy.GlobalReadOrAdmin, TenantScoping.QueryParam),
|
||||
new("GET", "global/metrics/rule-stats", EndpointPolicy.GlobalReadOrAdmin, TenantScoping.QueryParam),
|
||||
new("GET", "global/metrics/vulnerability", EndpointPolicy.GlobalReadOrAdmin, TenantScoping.QueryParam),
|
||||
new("GET", "global/metrics/mcp-usage", EndpointPolicy.GlobalReadOrAdmin, TenantScoping.QueryParam),
|
||||
new("GET", "global/metrics/mcp-usage/daily", EndpointPolicy.GlobalReadOrAdmin, TenantScoping.QueryParam),
|
||||
new("GET", "global/distress-reports", EndpointPolicy.GlobalReadOrAdmin),
|
||||
new("GET", "global/session-reports", EndpointPolicy.GlobalReadOrAdmin),
|
||||
new("GET", "global/session-reports", EndpointPolicy.GlobalReadOrAdmin, TenantScoping.QueryParam),
|
||||
new("GET", "global/session-reports/download-url", EndpointPolicy.GlobalReadOrAdmin),
|
||||
new("PATCH", "global/session-reports/{reportId}/note", EndpointPolicy.GlobalAdminOnly),
|
||||
new("GET", "global/rules/gather", EndpointPolicy.GlobalReadOrAdmin),
|
||||
new("GET", "global/rules/analyze", EndpointPolicy.GlobalReadOrAdmin),
|
||||
new("GET", "global/rules/gather", EndpointPolicy.GlobalReadOrAdmin, TenantScoping.QueryParam),
|
||||
new("GET", "global/rules/analyze", EndpointPolicy.GlobalReadOrAdmin, TenantScoping.QueryParam),
|
||||
new("GET", "global/devices/blocked", EndpointPolicy.GlobalReadOrAdmin),
|
||||
new("GET", "devices/blocked", EndpointPolicy.GlobalReadOrAdmin),
|
||||
new("POST", "devices/block", EndpointPolicy.GlobalAdminOnly),
|
||||
@@ -370,7 +379,7 @@ public static class EndpointAccessPolicyCatalog
|
||||
// The {tenantId} route param is the OFFBOARDED tenant whose data was archived;
|
||||
// the operator (GA) does not belong to that tenant. RouteParam scoping declares
|
||||
// that the route's tenantId is supplied from the URL, not from the caller's JWT.
|
||||
new("GET", "global/customs-archive", EndpointPolicy.GlobalReadOrAdmin),
|
||||
new("GET", "global/customs-archive", EndpointPolicy.GlobalReadOrAdmin, TenantScoping.QueryParam),
|
||||
new("GET", "global/customs-archive/{tenantId}/{historyRowKey}", EndpointPolicy.GlobalReadOrAdmin, TenantScoping.RouteParam),
|
||||
new("GET", "global/customs-archive/{tenantId}/{historyRowKey}/{archiveRowKey}", EndpointPolicy.GlobalReadOrAdmin, TenantScoping.RouteParam),
|
||||
new("DELETE", "global/customs-archive/{tenantId}/{historyRowKey}/{archiveRowKey}", EndpointPolicy.GlobalAdminOnly, TenantScoping.RouteParam),
|
||||
@@ -379,7 +388,7 @@ public static class EndpointAccessPolicyCatalog
|
||||
new("GET", "health/mcp", EndpointPolicy.AuthenticatedUser),
|
||||
new("POST", "rules/reseed-from-github", EndpointPolicy.GlobalAdminOnly),
|
||||
new("GET", "vulnerability/unmatched-software", EndpointPolicy.GlobalReadOrAdmin),
|
||||
new("GET", "vulnerability/software-inventory", EndpointPolicy.GlobalReadOrAdmin),
|
||||
new("GET", "vulnerability/software-inventory", EndpointPolicy.GlobalReadOrAdmin, TenantScoping.QueryParam),
|
||||
new("POST", "vulnerability/sync", EndpointPolicy.GlobalAdminOnly),
|
||||
new("POST", "vulnerability/sync-msrc", EndpointPolicy.GlobalAdminOnly),
|
||||
new("GET", "vulnerability/sync-status", EndpointPolicy.GlobalReadOrAdmin),
|
||||
@@ -413,7 +422,7 @@ public static class EndpointAccessPolicyCatalog
|
||||
new("GET", "notifications", EndpointPolicy.MemberRead, TenantScoping.Jwt),
|
||||
new("POST", "notifications/dismiss-all", EndpointPolicy.TenantAdminOrGA, TenantScoping.Jwt),
|
||||
new("POST", "notifications/{notificationId}/dismiss", EndpointPolicy.TenantAdminOrGA, TenantScoping.Jwt),
|
||||
new("GET", "global/ops-events", EndpointPolicy.GlobalReadOrAdmin),
|
||||
new("GET", "global/ops-events", EndpointPolicy.GlobalReadOrAdmin, TenantScoping.QueryParam),
|
||||
};
|
||||
|
||||
/// <summary>
|
||||
|
||||
Reference in New Issue
Block a user