diff --git a/src/Particular.LicensingComponent/Particular.LicensingComponent.csproj b/src/Particular.LicensingComponent/Particular.LicensingComponent.csproj index 4f87cd4a63..d1136120d9 100644 --- a/src/Particular.LicensingComponent/Particular.LicensingComponent.csproj +++ b/src/Particular.LicensingComponent/Particular.LicensingComponent.csproj @@ -13,6 +13,7 @@ + diff --git a/src/Particular.LicensingComponent/WebApi/LicensingController.cs b/src/Particular.LicensingComponent/WebApi/LicensingController.cs index 0b094fbb94..f898c0cc72 100644 --- a/src/Particular.LicensingComponent/WebApi/LicensingController.cs +++ b/src/Particular.LicensingComponent/WebApi/LicensingController.cs @@ -5,10 +5,12 @@ using System.Text.Json; using System.Threading; using Contracts; + using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Mvc; using Microsoft.Net.Http.Headers; using Particular.LicensingComponent.Report; + using ServiceControl.Infrastructure.Auth; [ApiController] [Route("api/licensing")] @@ -19,6 +21,7 @@ public LicensingController(IThroughputCollector throughputCollector) this.throughputCollector = throughputCollector; } + [Authorize(Policy = Permissions.ErrorThroughputView)] [Route("endpoints")] [HttpGet] public async Task> GetEndpointThroughput(CancellationToken cancellationToken) @@ -26,6 +29,7 @@ public async Task> GetEndpointThroughput(Cancell return await throughputCollector.GetThroughputSummary(cancellationToken); } + [Authorize(Policy = Permissions.ErrorThroughputManage)] [Route("endpoints/update")] [HttpPost] public async Task UpdateUserSelectionOnEndpointThroughput(List updateUserIndicators, CancellationToken cancellationToken) @@ -34,6 +38,7 @@ public async Task UpdateUserSelectionOnEndpointThroughput(List CanThroughputReportBeGenerated(CancellationToken cancellationToken) @@ -41,6 +46,7 @@ public async Task CanThroughputReportBeGenerated(Cancella return await throughputCollector.GetReportGenerationState(cancellationToken); } + [Authorize(Policy = Permissions.ErrorThroughputView)] [Route("report/file")] [HttpGet] public async Task GetThroughputReportFile([FromQuery(Name = "spVersion")] string? spVersion, CancellationToken cancellationToken) @@ -77,6 +83,7 @@ public async Task GetThroughputReportFile([FromQuery(Name = "spVersion")] string await JsonSerializer.SerializeAsync(entryStream, report, SerializationOptions.IndentedWithNoEscaping, cancellationToken); } + [Authorize(Policy = Permissions.ErrorThroughputView)] [Route("settings/info")] [HttpGet] public async Task GetThroughputSettingsInformation(CancellationToken cancellationToken) @@ -84,10 +91,12 @@ public async Task GetThroughputSettingsInformation return await throughputCollector.GetThroughputConnectionSettingsInformation(cancellationToken); } + [Authorize(Policy = Permissions.ErrorThroughputView)] [Route("settings/test")] [HttpGet] public async Task TestThroughputConnectionSettings(CancellationToken cancellationToken) => await throughputCollector.TestConnectionSettings(cancellationToken); + [Authorize(Policy = Permissions.ErrorThroughputView)] [Route("settings/masks")] [HttpGet] public async Task> GetMasks(CancellationToken cancellationToken) @@ -95,6 +104,7 @@ public async Task> GetMasks(CancellationToken cancellationToken) return await throughputCollector.GetReportMasks(cancellationToken); } + [Authorize(Policy = Permissions.ErrorThroughputManage)] [Route("settings/masks/update")] [HttpPost] public async Task UpdateMasks(List updateMasks, CancellationToken cancellationToken) diff --git a/src/ServiceControl.Audit/Auditing/MessagesView/GetMessages2Controller.cs b/src/ServiceControl.Audit/Auditing/MessagesView/GetMessages2Controller.cs index db187bcd77..5027f7f43f 100644 --- a/src/ServiceControl.Audit/Auditing/MessagesView/GetMessages2Controller.cs +++ b/src/ServiceControl.Audit/Auditing/MessagesView/GetMessages2Controller.cs @@ -5,13 +5,16 @@ namespace ServiceControl.Audit.Auditing.MessagesView; using System.Threading.Tasks; using Infrastructure; using Infrastructure.WebApi; +using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Mvc; using Persistence; +using ServiceControl.Infrastructure.Auth; [ApiController] [Route("api")] public class GetMessages2Controller(IAuditDataStore dataStore) : ControllerBase { + [Authorize(Policy = Permissions.AuditMessageView)] [Route("messages2")] [HttpGet] public async Task> GetAllMessages( diff --git a/src/ServiceControl.Audit/Auditing/MessagesView/GetMessagesController.cs b/src/ServiceControl.Audit/Auditing/MessagesView/GetMessagesController.cs index 7a81b7294d..e88d1fbb6f 100644 --- a/src/ServiceControl.Audit/Auditing/MessagesView/GetMessagesController.cs +++ b/src/ServiceControl.Audit/Auditing/MessagesView/GetMessagesController.cs @@ -9,11 +9,13 @@ namespace ServiceControl.Audit.Auditing.MessagesView using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Mvc; using Persistence; + using ServiceControl.Infrastructure.Auth; [ApiController] [Route("api")] public class GetMessagesController(IAuditDataStore dataStore) : ControllerBase { + [Authorize(Policy = Permissions.AuditMessageView)] [Route("messages")] [HttpGet] public async Task> GetAllMessages([FromQuery] PagingInfo pagingInfo, [FromQuery] SortInfo sortInfo, [FromQuery(Name = "include_system_messages")] bool includeSystemMessages, CancellationToken cancellationToken) @@ -23,6 +25,7 @@ public async Task> GetAllMessages([FromQuery] PagingInfo pag return result.Results; } + [Authorize(Policy = Permissions.AuditMessageView)] [Route("endpoints/{endpoint}/messages")] [HttpGet] public async Task> GetEndpointMessages([FromQuery] PagingInfo pagingInfo, [FromQuery] SortInfo sortInfo, [FromQuery(Name = "include_system_messages")] bool includeSystemMessages, string endpoint, CancellationToken cancellationToken) @@ -43,6 +46,7 @@ public async Task> GetEndpointAuditCounts([FromQuery] PagingIn return result.Results; } + [Authorize(Policy = Permissions.AuditMessageView)] [Route("messages/{id}/body")] [HttpGet] public async Task Get(string id, CancellationToken cancellationToken) @@ -69,6 +73,7 @@ public async Task Get(string id, CancellationToken cancellationTo return result.StringContent != null ? Content(result.StringContent, contentType) : File(result.StreamContent, contentType); } + [Authorize(Policy = Permissions.AuditMessageView)] [Route("messages/search")] [HttpGet] public async Task> Search([FromQuery] PagingInfo pagingInfo, [FromQuery] SortInfo sortInfo, string q, CancellationToken cancellationToken) @@ -78,6 +83,7 @@ public async Task> Search([FromQuery] PagingInfo pagingInfo, return result.Results; } + [Authorize(Policy = Permissions.AuditMessageView)] [Route("messages/search/{keyword}")] [HttpGet] public async Task> SearchByKeyWord([FromQuery] PagingInfo pagingInfo, [FromQuery] SortInfo sortInfo, string keyword, CancellationToken cancellationToken) @@ -87,6 +93,7 @@ public async Task> SearchByKeyWord([FromQuery] PagingInfo pa return result.Results; } + [Authorize(Policy = Permissions.AuditMessageView)] [Route("endpoints/{endpoint}/messages/search")] [HttpGet] public async Task> Search([FromQuery] PagingInfo pagingInfo, [FromQuery] SortInfo sortInfo, string endpoint, string q, CancellationToken cancellationToken) @@ -96,6 +103,7 @@ public async Task> Search([FromQuery] PagingInfo pagingInfo, return result.Results; } + [Authorize(Policy = Permissions.AuditMessageView)] [Route("endpoints/{endpoint}/messages/search/{keyword}")] [HttpGet] public async Task> SearchByKeyword([FromQuery] PagingInfo pagingInfo, [FromQuery] SortInfo sortInfo, string endpoint, string keyword, CancellationToken cancellationToken) diff --git a/src/ServiceControl.Audit/Auditing/MessagesView/MessagesConversationController.cs b/src/ServiceControl.Audit/Auditing/MessagesView/MessagesConversationController.cs index 4fcc04cd88..7ba20c1a11 100644 --- a/src/ServiceControl.Audit/Auditing/MessagesView/MessagesConversationController.cs +++ b/src/ServiceControl.Audit/Auditing/MessagesView/MessagesConversationController.cs @@ -5,13 +5,16 @@ namespace ServiceControl.Audit.Auditing.MessagesView using System.Threading.Tasks; using Infrastructure; using Infrastructure.WebApi; + using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Mvc; using Persistence; + using ServiceControl.Infrastructure.Auth; [ApiController] [Route("api")] public class MessagesConversationController(IAuditDataStore dataStore) : ControllerBase { + [Authorize(Policy = Permissions.AuditMessageView)] [Route("conversations/{conversationId}")] [HttpGet] public async Task> Get([FromQuery] PagingInfo pagingInfo, [FromQuery] SortInfo sortInfo, string conversationId, CancellationToken cancellationToken) diff --git a/src/ServiceControl.Audit/Connection/ConnectionController.cs b/src/ServiceControl.Audit/Connection/ConnectionController.cs index 75daeb593d..8d8d0fa4dc 100644 --- a/src/ServiceControl.Audit/Connection/ConnectionController.cs +++ b/src/ServiceControl.Audit/Connection/ConnectionController.cs @@ -2,7 +2,9 @@ namespace ServiceControl.Audit.Connection { using System.Text.Json; using Infrastructure.Settings; + using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Mvc; + using ServiceControl.Infrastructure.Auth; [ApiController] [Route("api")] @@ -11,6 +13,7 @@ public class ConnectionController(Settings settings) : ControllerBase // This controller doesn't use the default serialization settings because // ServicePulse and the Platform Connector Plugin expect the connection // details the be serialized and formatted in a specific way + [Authorize(Policy = Permissions.AuditConnectionView)] [Route("connection")] [HttpGet] public IActionResult GetConnectionDetails() => diff --git a/src/ServiceControl.Audit/Infrastructure/Hosting/Commands/RunCommand.cs b/src/ServiceControl.Audit/Infrastructure/Hosting/Commands/RunCommand.cs index 22e2fff776..d8229e8774 100644 --- a/src/ServiceControl.Audit/Infrastructure/Hosting/Commands/RunCommand.cs +++ b/src/ServiceControl.Audit/Infrastructure/Hosting/Commands/RunCommand.cs @@ -19,6 +19,7 @@ public override async Task Execute(HostArguments args, Settings settings) var hostBuilder = WebApplication.CreateBuilder(); hostBuilder.AddServiceControlAuthentication(settings.OpenIdConnectSettings); + hostBuilder.AddServiceControlAuthorization(settings.OpenIdConnectSettings.Enabled); hostBuilder.AddServiceControlHttps(settings.HttpsSettings); hostBuilder.AddServiceControlAudit((_, __) => { diff --git a/src/ServiceControl.Audit/Monitoring/KnownEndpoints/KnownEndpointsController.cs b/src/ServiceControl.Audit/Monitoring/KnownEndpoints/KnownEndpointsController.cs index 2ed0b2a278..3b7fd5871b 100644 --- a/src/ServiceControl.Audit/Monitoring/KnownEndpoints/KnownEndpointsController.cs +++ b/src/ServiceControl.Audit/Monitoring/KnownEndpoints/KnownEndpointsController.cs @@ -5,13 +5,16 @@ namespace ServiceControl.Audit.Monitoring using System.Threading.Tasks; using Infrastructure; using Infrastructure.WebApi; + using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Mvc; using Persistence; + using ServiceControl.Infrastructure.Auth; [ApiController] [Route("api")] public class KnownEndpointsController(IAuditDataStore dataStore) : ControllerBase { + [Authorize(Policy = Permissions.AuditEndpointView)] [Route("endpoints/known")] [HttpGet] public async Task> GetAll([FromQuery] PagingInfo pagingInfo, CancellationToken cancellationToken) diff --git a/src/ServiceControl.Audit/SagaAudit/SagasController.cs b/src/ServiceControl.Audit/SagaAudit/SagasController.cs index cd2356784d..3d92d5f913 100644 --- a/src/ServiceControl.Audit/SagaAudit/SagasController.cs +++ b/src/ServiceControl.Audit/SagaAudit/SagasController.cs @@ -5,14 +5,17 @@ namespace ServiceControl.Audit.SagaAudit using System.Threading.Tasks; using Infrastructure; using Infrastructure.WebApi; + using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Mvc; using Persistence; + using ServiceControl.Infrastructure.Auth; using ServiceControl.SagaAudit; [ApiController] [Route("api")] public class SagasController(IAuditDataStore dataStore) : ControllerBase { + [Authorize(Policy = Permissions.AuditSagaView)] [Route("sagas/{id}")] [HttpGet] public async Task Sagas([FromQuery] PagingInfo pagingInfo, Guid id, CancellationToken cancellationToken) diff --git a/src/ServiceControl.Hosting/Auth/PermissionAuthorizationExtensions.cs b/src/ServiceControl.Hosting/Auth/PermissionAuthorizationExtensions.cs new file mode 100644 index 0000000000..07229849eb --- /dev/null +++ b/src/ServiceControl.Hosting/Auth/PermissionAuthorizationExtensions.cs @@ -0,0 +1,43 @@ +#nullable enable +namespace ServiceControl.Hosting.Auth; + +using Microsoft.AspNetCore.Authorization; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Hosting; +using Microsoft.Extensions.Options; + +/// +/// Registers the permission-based policy authorization services: a dynamic +/// that resolves [Authorize(Policy = "<permission>")] +/// attributes, and — when OIDC is enabled — the that evaluates them +/// against the user's roles. +/// +/// The provider is registered unconditionally so the policy attributes resolve in every configuration +/// (without it, annotated endpoints fail with "AuthorizationPolicy not found"). When OIDC is disabled the +/// provider returns allow-all policies that carry no requirement, so the verb handler is not registered. +/// Wire this into every instance that hosts annotated controllers (Error, Audit, Monitoring). +/// +/// +public static class PermissionAuthorizationExtensions +{ + public static void AddServiceControlAuthorization(this IHostApplicationBuilder hostBuilder, bool oidcEnabled) + { + var services = hostBuilder.Services; + + // Ensure the authorization core services and options are present (idempotent). + services.AddAuthorization(); + + // Resolve permission policy names dynamically. Registered last so it supersedes the default + // policy provider registered by AddAuthorization(). When OIDC is disabled it returns allow-all + // policies (no requirement); when enabled it emits a PermissionRequirement for the verb handler. + services.AddSingleton(sp => + new PermissionPolicyProvider(sp.GetRequiredService>(), oidcEnabled)); + + // The role-based handler is only needed when OIDC is enabled — otherwise the provider produces + // no PermissionRequirement for it to evaluate. + if (oidcEnabled) + { + services.AddSingleton(); + } + } +} diff --git a/src/ServiceControl.Hosting/Auth/PermissionPolicyProvider.cs b/src/ServiceControl.Hosting/Auth/PermissionPolicyProvider.cs new file mode 100644 index 0000000000..1d5a52d4ff --- /dev/null +++ b/src/ServiceControl.Hosting/Auth/PermissionPolicyProvider.cs @@ -0,0 +1,59 @@ +#nullable enable +namespace ServiceControl.Hosting.Auth; + +using System; +using System.Collections.Frozen; +using System.Collections.Generic; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Authorization; +using Microsoft.Extensions.Options; +using ServiceControl.Infrastructure.Auth; + +/// +/// A dynamic that resolves a verb-level authorization policy +/// for each known permission string (e.g. error:messages:retry). +/// +/// The set of valid policy names is known up front (), so every policy is +/// built once at construction into a . The framework +/// calls on every request to a protected endpoint, so this makes that call +/// an O(1) lookup with no per-request policy allocation. (Authorization policies and requirements are +/// immutable, so the prebuilt instances are safely shared across all requests.) +/// +/// +/// When OIDC is enabled each permission maps to a policy carrying a +/// (evaluated by ). When OIDC is disabled the platform runs +/// unauthenticated, so every permission maps to a shared allow-all policy — no requirement, no handler. +/// Unknown policy names resolve to ; the default and fallback policies are +/// delegated to the configured . +/// +/// +public sealed class PermissionPolicyProvider(IOptions authorizationOptions, bool oidcEnabled) + : IAuthorizationPolicyProvider +{ + // Carries no requirement, so it succeeds without any IAuthorizationHandler being registered. + static readonly AuthorizationPolicy AllowAll = + new AuthorizationPolicyBuilder().RequireAssertion(_ => true).Build(); + + readonly FrozenDictionary policies = BuildPolicies(oidcEnabled); + + static FrozenDictionary BuildPolicies(bool oidcEnabled) => + Permissions.All.ToFrozenDictionary( + permission => permission, + permission => oidcEnabled + ? new AuthorizationPolicyBuilder().AddRequirements(new PermissionRequirement(permission)).Build() + : AllowAll, + StringComparer.Ordinal); + + public Task GetPolicyAsync(string policyName) => + Task.FromResult(policies.GetValueOrDefault(policyName)); + + public Task GetDefaultPolicyAsync() + { + var defaultPolicy = authorizationOptions.Value.DefaultPolicy + ?? new AuthorizationPolicyBuilder().RequireAuthenticatedUser().Build(); + return Task.FromResult(defaultPolicy); + } + + public Task GetFallbackPolicyAsync() + => Task.FromResult(authorizationOptions.Value.FallbackPolicy); +} diff --git a/src/ServiceControl.Hosting/Auth/PermissionRequirement.cs b/src/ServiceControl.Hosting/Auth/PermissionRequirement.cs new file mode 100644 index 0000000000..77039457b7 --- /dev/null +++ b/src/ServiceControl.Hosting/Auth/PermissionRequirement.cs @@ -0,0 +1,11 @@ +#nullable enable +namespace ServiceControl.Hosting.Auth; + +using Microsoft.AspNetCore.Authorization; + +/// +/// An that carries the permission string enforced by a +/// [Authorize(Policy = "<permission>")] attribute (e.g. error:messages:view). +/// Evaluated by . +/// +public sealed record PermissionRequirement(string Permission) : IAuthorizationRequirement; diff --git a/src/ServiceControl.Hosting/Auth/PermissionVerbHandler.cs b/src/ServiceControl.Hosting/Auth/PermissionVerbHandler.cs new file mode 100644 index 0000000000..f246234c7b --- /dev/null +++ b/src/ServiceControl.Hosting/Auth/PermissionVerbHandler.cs @@ -0,0 +1,44 @@ +#nullable enable +namespace ServiceControl.Hosting.Auth; + +using System.Linq; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Authorization; +using ServiceControl.Infrastructure.Auth; + +/// +/// Verb-level authorization handler for . It resolves the user's +/// roles and checks them against the hardcoded policy: the user must hold +/// a role (e.g. reader / writer) that grants the requested permission. +/// +/// Only registered — and only reached — when OIDC is enabled. When it is disabled, +/// returns an allow-all policy that carries no +/// , so this handler is not needed. +/// +/// +public sealed class PermissionVerbHandler : AuthorizationHandler +{ + // TODO: The claim that carries a user's roles is identity-provider specific and must become + // configurable (per-IdP) rather than hardcoded. Roles are expected as a flat, multivalued claim; + // the token handler splits a top-level JSON array into individual claims, so no parsing is needed. + // For Keycloak, add a "User Realm Role" protocol mapper with Multivalued = ON and Token Claim Name + // = "roles" (a dotted name like "realm_access.roles" would nest it into an object instead). + const string RoleClaimType = "roles"; + + protected override Task HandleRequirementAsync( + AuthorizationHandlerContext context, + PermissionRequirement requirement) + { + var roles = context.User.FindAll(RoleClaimType).Select(claim => claim.Value); + + + // TODO: Although plural, likely roles will only contain a single value unless we want to define a role for each instance but likely customers don't care about instances + if (RolePermissions.IsGranted(roles, requirement.Permission)) + { + context.Succeed(requirement); + } + + // Otherwise leave the requirement unmet → the request is denied (403/401). + return Task.CompletedTask; + } +} diff --git a/src/ServiceControl.Infrastructure/Auth/Permissions.cs b/src/ServiceControl.Infrastructure/Auth/Permissions.cs new file mode 100644 index 0000000000..006fccf0f5 --- /dev/null +++ b/src/ServiceControl.Infrastructure/Auth/Permissions.cs @@ -0,0 +1,137 @@ +#nullable enable +namespace ServiceControl.Infrastructure.Auth; + +using System.Collections.Generic; +using System.Reflection; + +/// +/// Catalogue of all known permission constants in the format instance:resource:action. +/// Each ServiceControl instance (error/audit/monitoring) is a separate process and namespaces its +/// permissions with an instance prefix. +/// +/// The set is automatically derived from all public const string +/// fields on this class, so adding a new constant is sufficient — no separate registration needed. +/// +/// +public static class Permissions +{ + // ───────────────────────────── Error instance (Primary) ───────────────────────────── + + /// Messages area — viewing, retrying, archiving, and editing failed messages. + public const string ErrorMessagesView = "error:messages:view"; + /// + public const string ErrorMessagesRetry = "error:messages:retry"; + /// + public const string ErrorMessagesArchive = "error:messages:archive"; + /// + public const string ErrorMessagesUnarchive = "error:messages:unarchive"; + /// + public const string ErrorMessagesEdit = "error:messages:edit"; + + /// Recoverability groups area — viewing, retrying, archiving, and unarchiving failure groups. + public const string ErrorRecoverabilityGroupsView = "error:recoverabilitygroups:view"; + /// + public const string ErrorRecoverabilityGroupsRetry = "error:recoverabilitygroups:retry"; + /// + public const string ErrorRecoverabilityGroupsArchive = "error:recoverabilitygroups:archive"; + /// + public const string ErrorRecoverabilityGroupsUnarchive = "error:recoverabilitygroups:unarchive"; + + /// Endpoints area — viewing, managing, and deleting monitored endpoints. + public const string ErrorEndpointsView = "error:endpoints:view"; + /// + public const string ErrorEndpointsManage = "error:endpoints:manage"; + /// + public const string ErrorEndpointsDelete = "error:endpoints:delete"; + + /// Heartbeats area — viewing heartbeat status for endpoints. + public const string ErrorHeartbeatsView = "error:heartbeats:view"; + + /// Custom checks area — viewing and deleting custom check results. + public const string ErrorCustomChecksView = "error:customchecks:view"; + /// + public const string ErrorCustomChecksDelete = "error:customchecks:delete"; + + /// Sagas area — viewing saga audit data. + public const string ErrorSagasView = "error:sagas:view"; + + /// Event log area — viewing the event log. + public const string ErrorEventLogView = "error:eventlog:view"; + + /// Licensing area — viewing and managing license configuration. + public const string ErrorLicensingView = "error:licensing:view"; + /// + public const string ErrorLicensingManage = "error:licensing:manage"; + + /// Notifications area — viewing, managing, and testing notification settings. + public const string ErrorNotificationsView = "error:notifications:view"; + /// + public const string ErrorNotificationsManage = "error:notifications:manage"; + /// + public const string ErrorNotificationsTest = "error:notifications:test"; + + /// Retry redirects area — viewing and managing message redirect rules. + public const string ErrorRedirectsView = "error:redirects:view"; + /// + public const string ErrorRedirectsManage = "error:redirects:manage"; + + /// Queue addresses area — viewing and deleting queue address entries. + public const string ErrorQueuesView = "error:queues:view"; + /// + public const string ErrorQueuesDelete = "error:queues:delete"; + + /// Throughput area — viewing and managing throughput reports and settings. + public const string ErrorThroughputView = "error:throughput:view"; + /// + public const string ErrorThroughputManage = "error:throughput:manage"; + + /// Platform connections area — viewing and managing broker/platform connection settings. + public const string ErrorConnectionsView = "error:connections:view"; + /// + public const string ErrorConnectionsManage = "error:connections:manage"; + + // ───────────────────────────── Audit instance ───────────────────────────── + + /// Audit instance (separate process) — read-only audit message log. + public const string AuditMessageView = "audit:message:view"; + /// Audit instance — viewing platform connection details. + public const string AuditConnectionView = "audit:connection:view"; + /// Audit instance — viewing known endpoints. + public const string AuditEndpointView = "audit:endpoint:view"; + /// Audit instance — viewing saga audit data. + public const string AuditSagaView = "audit:saga:view"; + + // ───────────────────────────── Monitoring instance ───────────────────────────── + + /// Monitoring instance (separate process) — viewing endpoint metrics. + public const string MonitoringEndpointView = "monitoring:endpoint:view"; + /// Monitoring instance — removing a monitored endpoint instance. + public const string MonitoringEndpointDelete = "monitoring:endpoint:delete"; + /// Monitoring instance — viewing platform connection details. + public const string MonitoringConnectionView = "monitoring:connection:view"; + /// Monitoring instance — viewing license status. + public const string MonitoringLicenseView = "monitoring:license:view"; + + /// + /// The complete set of known permissions, derived from all public const string + /// fields declared on this class. Used by the policy provider and coverage tests. + /// + public static readonly IReadOnlySet All = BuildAll(); + + static IReadOnlySet BuildAll() + { + var set = new HashSet(); + foreach (var field in typeof(Permissions).GetFields(BindingFlags.Public | BindingFlags.Static)) + { + if (field.IsLiteral && !field.IsInitOnly && field.FieldType == typeof(string)) + { + var value = (string?)field.GetValue(null); + if (value != null) + { + set.Add(value); + } + } + } + return set; + } +} diff --git a/src/ServiceControl.Infrastructure/Auth/RolePermissions.cs b/src/ServiceControl.Infrastructure/Auth/RolePermissions.cs new file mode 100644 index 0000000000..f0ff2028a1 --- /dev/null +++ b/src/ServiceControl.Infrastructure/Auth/RolePermissions.cs @@ -0,0 +1,127 @@ +#nullable enable +namespace ServiceControl.Infrastructure.Auth; + +using System; +using System.Collections.Frozen; +using System.Collections.Generic; +using System.Linq; + +/// +/// Hardcoded role → permission policy. Two roles for now: +/// +/// reader — granted every *:*:view permission (read-only access). +/// writer — granted every permission (*:*:*). +/// +/// The wildcard patterns (* is a colon-segment wildcard) are the source of truth, but they are +/// expanded once at type initialization against into a concrete, +/// immutable of granted permissions per role. As a result both +/// and are O(1) hash lookups with no +/// per-call pattern matching or allocation. +/// +/// TODO: interim hardcoded model — replace with a configurable role/permission mapping (loaded from +/// configuration or the IdP) when more than these two coarse roles are needed. +/// +/// +public static class RolePermissions +{ + /// Read-only role: every *:*:view permission. + public const string Reader = "reader"; + + /// Full-access role: every permission. + public const string Writer = "writer"; + + // Source of truth: the wildcard pattern(s) each role grants. + static readonly Dictionary RolePatterns = new(StringComparer.OrdinalIgnoreCase) + { + [Reader] = ["*:*:view"], + [Writer] = ["*:*:*"], + }; + + // Expanded once against the full permission catalogue: role -> concrete granted permissions. + static readonly FrozenDictionary> PermissionsByRole = Expand(); + + /// + /// Returns if any of the supplied grants the + /// requested . O(1) per role — a frozen-set membership test. + /// + public static bool IsGranted(IEnumerable roles, string permission) + { + foreach (var role in roles) + { + if (PermissionsByRole.TryGetValue(role, out var granted) && granted.Contains(permission)) + { + return true; + } + } + + return false; + } + + /// + /// The complete set of permissions granted to a single role (empty if the role is unknown). + /// O(1) and allocation-free — returns the precomputed frozen set. + /// + public static IReadOnlySet GetPermissions(string role) => + PermissionsByRole.TryGetValue(role, out var granted) ? granted : FrozenSet.Empty; + + /// + /// The union of permissions granted across several . Allocation-free for the + /// common single-role case; only the multi-role union allocates. + /// + public static IReadOnlySet GetPermissions(IEnumerable roles) + { + var list = roles as IReadOnlyList ?? roles.ToList(); + if (list.Count <= 1) + { + return list.Count == 0 ? FrozenSet.Empty : GetPermissions(list[0]); + } + + var union = new HashSet(StringComparer.Ordinal); + foreach (var role in list) + { + if (PermissionsByRole.TryGetValue(role, out var granted)) + { + union.UnionWith(granted); + } + } + + return union; + } + + static FrozenDictionary> Expand() + { + var expanded = new Dictionary>(StringComparer.OrdinalIgnoreCase); + + foreach (var (role, patterns) in RolePatterns) + { + expanded[role] = Permissions.All + .Where(permission => patterns.Any(pattern => Matches(pattern, permission))) + .ToFrozenSet(StringComparer.Ordinal); + } + + return expanded.ToFrozenDictionary(StringComparer.OrdinalIgnoreCase); + } + + /// Matches a colon-delimited permission against a pattern where * is a segment wildcard. + static bool Matches(string pattern, string permission) + { + var patternSegments = pattern.Split(':'); + var permissionSegments = permission.Split(':'); + + if (patternSegments.Length != permissionSegments.Length) + { + return false; + } + + for (var i = 0; i < patternSegments.Length; i++) + { + if (patternSegments[i] != "*" + && !string.Equals(patternSegments[i], permissionSegments[i], StringComparison.OrdinalIgnoreCase)) + { + return false; + } + } + + return true; + } +} diff --git a/src/ServiceControl.Monitoring/Connection/ConnectionController.cs b/src/ServiceControl.Monitoring/Connection/ConnectionController.cs index e765007b73..28978672a6 100644 --- a/src/ServiceControl.Monitoring/Connection/ConnectionController.cs +++ b/src/ServiceControl.Monitoring/Connection/ConnectionController.cs @@ -2,8 +2,10 @@ { using System; using System.Text.Json; + using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Mvc; using NServiceBus; + using ServiceControl.Infrastructure.Auth; [ApiController] public class ConnectionController(ReceiveAddresses receiveAddresses) : ControllerBase @@ -11,6 +13,7 @@ public class ConnectionController(ReceiveAddresses receiveAddresses) : Controlle readonly string mainInputQueue = receiveAddresses.MainReceiveAddress; readonly TimeSpan defaultInterval = TimeSpan.FromSeconds(1); + [Authorize(Policy = Permissions.MonitoringConnectionView)] [Route("connection")] [HttpGet] public IActionResult GetConnectionDetails() => diff --git a/src/ServiceControl.Monitoring/Hosting/Commands/RunCommand.cs b/src/ServiceControl.Monitoring/Hosting/Commands/RunCommand.cs index ca648ac222..da23814397 100644 --- a/src/ServiceControl.Monitoring/Hosting/Commands/RunCommand.cs +++ b/src/ServiceControl.Monitoring/Hosting/Commands/RunCommand.cs @@ -16,6 +16,7 @@ public override async Task Execute(HostArguments args, Settings settings) var hostBuilder = WebApplication.CreateBuilder(); hostBuilder.AddServiceControlAuthentication(settings.OpenIdConnectSettings); + hostBuilder.AddServiceControlAuthorization(settings.OpenIdConnectSettings.Enabled); hostBuilder.AddServiceControlHttps(settings.HttpsSettings); hostBuilder.AddServiceControlMonitoring((_, __) => Task.CompletedTask, settings, endpointConfiguration); hostBuilder.AddServiceControlMonitoringApi(); diff --git a/src/ServiceControl.Monitoring/Http/Diagrams/DiagramApiController.cs b/src/ServiceControl.Monitoring/Http/Diagrams/DiagramApiController.cs index 134bf92716..04bd58ac86 100644 --- a/src/ServiceControl.Monitoring/Http/Diagrams/DiagramApiController.cs +++ b/src/ServiceControl.Monitoring/Http/Diagrams/DiagramApiController.cs @@ -1,21 +1,26 @@ namespace ServiceControl.Monitoring.Http.Diagrams { using Infrastructure.Api; + using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Mvc; + using ServiceControl.Infrastructure.Auth; [ApiController] public class DiagramApiController(IEndpointMetricsApi endpointMetricsApi) : ControllerBase { + [Authorize(Policy = Permissions.MonitoringEndpointView)] [Route("monitored-endpoints")] [HttpGet] public MonitoredEndpoint[] GetAllEndpointsMetrics([FromQuery] int? history = null) => endpointMetricsApi.GetAllEndpointsMetrics(history); + [Authorize(Policy = Permissions.MonitoringEndpointView)] [Route("monitored-endpoints/{endpointName}")] [HttpGet] public ActionResult GetSingleEndpointMetrics(string endpointName, [FromQuery] int? history = null) => endpointMetricsApi.GetSingleEndpointMetrics(endpointName, history); + [Authorize(Policy = Permissions.MonitoringEndpointDelete)] [Route("monitored-instance/{endpointName}/{instanceId}")] [HttpDelete] public IActionResult DeleteEndpointInstance(string endpointName, string instanceId) @@ -25,6 +30,7 @@ public IActionResult DeleteEndpointInstance(string endpointName, string instance return Ok(); } + [Authorize(Policy = Permissions.MonitoringEndpointView)] [Route("monitored-endpoints/disconnected")] [HttpGet] public ActionResult DisconnectedEndpointCount() => endpointMetricsApi.DisconnectedEndpointCount(); diff --git a/src/ServiceControl.Monitoring/Http/LicenseController.cs b/src/ServiceControl.Monitoring/Http/LicenseController.cs index d76e2e9361..55bcdfbbb5 100644 --- a/src/ServiceControl.Monitoring/Http/LicenseController.cs +++ b/src/ServiceControl.Monitoring/Http/LicenseController.cs @@ -1,11 +1,14 @@ namespace ServiceControl.Monitoring.Http { + using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Mvc; + using ServiceControl.Infrastructure.Auth; using ServiceControl.Monitoring.Licensing; [ApiController] public class LicenseController(ActiveLicense activeLicense) : ControllerBase { + [Authorize(Policy = Permissions.MonitoringLicenseView)] [Route("license")] [HttpGet] public ActionResult License(bool refresh) diff --git a/src/ServiceControl/CompositeViews/Messages/GetMessages2Controller.cs b/src/ServiceControl/CompositeViews/Messages/GetMessages2Controller.cs index 1ee79cfbbc..eb72656fb1 100644 --- a/src/ServiceControl/CompositeViews/Messages/GetMessages2Controller.cs +++ b/src/ServiceControl/CompositeViews/Messages/GetMessages2Controller.cs @@ -2,7 +2,9 @@ namespace ServiceControl.CompositeViews.Messages; using System.Collections.Generic; using System.Threading.Tasks; +using Infrastructure.Auth; using Infrastructure.WebApi; +using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Http.Extensions; using Microsoft.AspNetCore.Mvc; using Persistence.Infrastructure; @@ -16,6 +18,7 @@ public class GetMessages2Controller( SearchEndpointApi searchEndpointApi) : ControllerBase { + [Authorize(Policy = Permissions.ErrorMessagesView)] [Route("messages2")] [HttpGet] public async Task> Messages( diff --git a/src/ServiceControl/CompositeViews/Messages/GetMessagesByConversationController.cs b/src/ServiceControl/CompositeViews/Messages/GetMessagesByConversationController.cs index 7bd650b453..ab18a6da9e 100644 --- a/src/ServiceControl/CompositeViews/Messages/GetMessagesByConversationController.cs +++ b/src/ServiceControl/CompositeViews/Messages/GetMessagesByConversationController.cs @@ -2,7 +2,9 @@ { using System.Collections.Generic; using System.Threading.Tasks; + using Infrastructure.Auth; using Infrastructure.WebApi; + using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Http.Extensions; using Microsoft.AspNetCore.Mvc; using Persistence.Infrastructure; @@ -12,6 +14,7 @@ public class GetMessagesByConversationController(MessagesByConversationApi byConversationApi) : ControllerBase { + [Authorize(Policy = Permissions.ErrorMessagesView)] [Route("conversations/{conversationId:required:minlength(1)}")] [HttpGet] public async Task> Messages([FromQuery] PagingInfo pagingInfo, diff --git a/src/ServiceControl/CompositeViews/Messages/GetMessagesController.cs b/src/ServiceControl/CompositeViews/Messages/GetMessagesController.cs index e7133ba20a..d586fe34cd 100644 --- a/src/ServiceControl/CompositeViews/Messages/GetMessagesController.cs +++ b/src/ServiceControl/CompositeViews/Messages/GetMessagesController.cs @@ -5,8 +5,10 @@ namespace ServiceControl.CompositeViews.Messages using System.Net.Http; using System.Threading.Tasks; using Api.Contracts; + using Infrastructure.Auth; using Infrastructure.WebApi; using MessageCounting; + using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Http.Extensions; using Microsoft.AspNetCore.Mvc; @@ -33,6 +35,7 @@ public class GetMessagesController( ILogger logger) : ControllerBase { + [Authorize(Policy = Permissions.ErrorMessagesView)] [Route("messages")] [HttpGet] public async Task> Messages([FromQuery] PagingInfo pagingInfo, @@ -48,6 +51,7 @@ public async Task> Messages([FromQuery] PagingInfo pagingInf return result.Results; } + [Authorize(Policy = Permissions.ErrorMessagesView)] [Route("endpoints/{endpoint}/messages")] [HttpGet] public async Task> MessagesForEndpoint([FromQuery] PagingInfo pagingInfo, @@ -64,6 +68,7 @@ public async Task> MessagesForEndpoint([FromQuery] PagingInf } // the endpoint name is needed in the route to match the route and forward it as path and query to the remotes + [Authorize(Policy = Permissions.ErrorMessagesView)] [Route("endpoints/{endpoint}/audit-count")] [HttpGet] public async Task> GetEndpointAuditCounts([FromQuery] PagingInfo pagingInfo, string endpoint) @@ -75,6 +80,7 @@ public async Task> GetEndpointAuditCounts([FromQuery] PagingIn return result.Results; } + [Authorize(Policy = Permissions.ErrorMessagesView)] [Route("messages/{id}/body")] [HttpGet] public async Task Get(string id, [FromQuery(Name = "instance_id")] string instanceId) @@ -114,6 +120,7 @@ public async Task Get(string id, [FromQuery(Name = "instance_id") return Empty; } + [Authorize(Policy = Permissions.ErrorMessagesView)] [Route("messages/search")] [HttpGet] public async Task> Search([FromQuery] PagingInfo pagingInfo, [FromQuery] SortInfo sortInfo, @@ -126,6 +133,7 @@ public async Task> Search([FromQuery] PagingInfo pagingInfo, return result.Results; } + [Authorize(Policy = Permissions.ErrorMessagesView)] [Route("messages/search/{keyword}")] [HttpGet] public async Task> SearchByKeyWord([FromQuery] PagingInfo pagingInfo, @@ -139,6 +147,7 @@ public async Task> SearchByKeyWord([FromQuery] PagingInfo pa return result.Results; } + [Authorize(Policy = Permissions.ErrorMessagesView)] [Route("endpoints/{endpoint}/messages/search")] [HttpGet] public async Task> Search([FromQuery] PagingInfo pagingInfo, [FromQuery] SortInfo sortInfo, @@ -151,6 +160,7 @@ public async Task> Search([FromQuery] PagingInfo pagingInfo, return result.Results; } + [Authorize(Policy = Permissions.ErrorMessagesView)] [Route("endpoints/{endpoint}/messages/search/{keyword}")] [HttpGet] public async Task> SearchByKeyword([FromQuery] PagingInfo pagingInfo, diff --git a/src/ServiceControl/Connection/ConnectionController.cs b/src/ServiceControl/Connection/ConnectionController.cs index a85522a84c..b87d4d8dc7 100644 --- a/src/ServiceControl/Connection/ConnectionController.cs +++ b/src/ServiceControl/Connection/ConnectionController.cs @@ -4,6 +4,8 @@ using System.Text.Json; using System.Text.Json.Serialization; using System.Threading.Tasks; + using Infrastructure.Auth; + using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Mvc; [ApiController] @@ -13,6 +15,7 @@ public class ConnectionController(IPlatformConnectionBuilder builder) : Controll // This controller doesn't use the default serialization settings because // ServicePulse and the Platform Connector Plugin expect the connection // details the be serialized and formatted in a specific way + [Authorize(Policy = Permissions.ErrorConnectionsView)] [Route("connection")] [HttpGet] public async Task GetConnectionDetails() diff --git a/src/ServiceControl/CustomChecks/Web/CustomCheckController.cs b/src/ServiceControl/CustomChecks/Web/CustomCheckController.cs index 0cc06bbd0c..f9fb690757 100644 --- a/src/ServiceControl/CustomChecks/Web/CustomCheckController.cs +++ b/src/ServiceControl/CustomChecks/Web/CustomCheckController.cs @@ -4,7 +4,9 @@ using System.Collections.Generic; using System.Threading.Tasks; using Contracts.CustomChecks; + using Infrastructure.Auth; using Infrastructure.WebApi; + using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Mvc; using NServiceBus; using ServiceControl.Persistence; @@ -15,6 +17,7 @@ public class CustomCheckController(ICustomChecksDataStore checksDataStore, IMessageSession session) : ControllerBase { + [Authorize(Policy = Permissions.ErrorCustomChecksView)] [Route("customchecks")] [HttpGet] public async Task> CustomChecks([FromQuery] PagingInfo pagingInfo, string status = null) @@ -27,6 +30,7 @@ public async Task> CustomChecks([FromQuery] PagingInfo paging return stats.Results; } + [Authorize(Policy = Permissions.ErrorCustomChecksDelete)] [Route("customchecks/{id}")] [HttpDelete] public async Task Delete(Guid id) diff --git a/src/ServiceControl/EventLog/EventLogApiController.cs b/src/ServiceControl/EventLog/EventLogApiController.cs index 1872154be7..605efc453d 100644 --- a/src/ServiceControl/EventLog/EventLogApiController.cs +++ b/src/ServiceControl/EventLog/EventLogApiController.cs @@ -2,7 +2,9 @@ { using System.Collections.Generic; using System.Threading.Tasks; + using Infrastructure.Auth; using Infrastructure.WebApi; + using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Mvc; using Persistence.Infrastructure; using ServiceControl.Persistence; @@ -11,6 +13,7 @@ [Route("api")] public class EventLogApiController(IEventLogDataStore logDataStore) : ControllerBase { + [Authorize(Policy = Permissions.ErrorEventLogView)] [Route("eventlogitems")] [HttpGet] public async Task> Items([FromQuery] PagingInfo pagingInfo) diff --git a/src/ServiceControl/Hosting/Commands/RunCommand.cs b/src/ServiceControl/Hosting/Commands/RunCommand.cs index ebc08958cf..1f895ec9d3 100644 --- a/src/ServiceControl/Hosting/Commands/RunCommand.cs +++ b/src/ServiceControl/Hosting/Commands/RunCommand.cs @@ -25,6 +25,7 @@ public override async Task Execute(HostArguments args, Settings settings) var hostBuilder = WebApplication.CreateBuilder(); hostBuilder.AddServiceControlAuthentication(settings.OpenIdConnectSettings); + hostBuilder.AddServiceControlAuthorization(settings.OpenIdConnectSettings.Enabled); hostBuilder.AddServiceControlHttps(settings.HttpsSettings); hostBuilder.AddServiceControl(settings, endpointConfiguration); hostBuilder.AddServiceControlApi(settings.CorsSettings); diff --git a/src/ServiceControl/Licensing/LicenseController.cs b/src/ServiceControl/Licensing/LicenseController.cs index 15539d9db0..9f9e56cb6e 100644 --- a/src/ServiceControl/Licensing/LicenseController.cs +++ b/src/ServiceControl/Licensing/LicenseController.cs @@ -2,6 +2,8 @@ { using System.Threading; using System.Threading.Tasks; + using Infrastructure.Auth; + using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Mvc; using Monitoring.HeartbeatMonitoring; using Particular.ServiceControl.Licensing; @@ -11,6 +13,7 @@ [Route("api")] public class LicenseController(ActiveLicense activeLicense, Settings settings, MassTransitConnectorHeartbeatStatus connectorHeartbeatStatus) : ControllerBase { + [Authorize(Policy = Permissions.ErrorLicensingView)] [HttpGet] [Route("license")] public async Task> License(bool refresh, string clientName, CancellationToken cancellationToken) diff --git a/src/ServiceControl/MessageFailures/Api/ArchiveMessagesController.cs b/src/ServiceControl/MessageFailures/Api/ArchiveMessagesController.cs index bad1ec4cf5..84384bec6f 100644 --- a/src/ServiceControl/MessageFailures/Api/ArchiveMessagesController.cs +++ b/src/ServiceControl/MessageFailures/Api/ArchiveMessagesController.cs @@ -2,8 +2,10 @@ namespace ServiceControl.MessageFailures.Api { using System.Linq; using System.Threading.Tasks; + using Infrastructure.Auth; using Infrastructure.WebApi; using InternalMessages; + using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Mvc; using NServiceBus; using ServiceControl.Persistence; @@ -13,6 +15,7 @@ namespace ServiceControl.MessageFailures.Api [Route("api")] public class ArchiveMessagesController(IMessageSession messageSession, IErrorMessageDataStore dataStore) : ControllerBase { + [Authorize(Policy = Permissions.ErrorMessagesArchive)] [Route("errors/archive")] [HttpPost] [HttpPatch] @@ -34,6 +37,7 @@ public async Task ArchiveBatch(string[] messageIds) return Accepted(); } + [Authorize(Policy = Permissions.ErrorMessagesView)] [Route("errors/groups/{classifier?}")] [HttpGet] public async Task GetArchiveMessageGroups(string classifier = "Exception Type and Stack Trace") @@ -45,6 +49,7 @@ public async Task GetArchiveMessageGroups(string classifier = "Ex return Ok(results); } + [Authorize(Policy = Permissions.ErrorMessagesArchive)] [Route("errors/{messageId:required:minlength(1)}/archive")] [HttpPost] [HttpPatch] @@ -55,6 +60,7 @@ public async Task Archive(string messageId) return Accepted(); } + [Authorize(Policy = Permissions.ErrorMessagesView)] [Route("archive/groups/id/{groupId:required:minlength(1)}")] [HttpGet] public async Task> GetGroup(string groupId, string status = default, string modified = default) diff --git a/src/ServiceControl/MessageFailures/Api/EditFailedMessagesController.cs b/src/ServiceControl/MessageFailures/Api/EditFailedMessagesController.cs index 3ce42fc3ff..d5f5d587f6 100644 --- a/src/ServiceControl/MessageFailures/Api/EditFailedMessagesController.cs +++ b/src/ServiceControl/MessageFailures/Api/EditFailedMessagesController.cs @@ -5,6 +5,8 @@ using System.Linq; using System.Text; using System.Threading.Tasks; + using Infrastructure.Auth; + using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Mvc; using Microsoft.Extensions.Logging; using NServiceBus; @@ -21,10 +23,12 @@ public class EditFailedMessagesController( ILogger logger) : ControllerBase { + [Authorize(Policy = Permissions.ErrorMessagesEdit)] [Route("edit/config")] [HttpGet] public EditConfigurationModel Config() => GetEditConfiguration(); + [Authorize(Policy = Permissions.ErrorMessagesEdit)] [Route("edit/{failedMessageId:required:minlength(1)}")] [HttpPost] public async Task> Edit(string failedMessageId, [FromBody] EditMessageModel edit) diff --git a/src/ServiceControl/MessageFailures/Api/GetAllErrorsController.cs b/src/ServiceControl/MessageFailures/Api/GetAllErrorsController.cs index 60f9f08ca9..06c7260d26 100644 --- a/src/ServiceControl/MessageFailures/Api/GetAllErrorsController.cs +++ b/src/ServiceControl/MessageFailures/Api/GetAllErrorsController.cs @@ -2,7 +2,9 @@ { using System.Collections.Generic; using System.Threading.Tasks; + using Infrastructure.Auth; using Infrastructure.WebApi; + using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Mvc; using Persistence.Infrastructure; using ServiceControl.Persistence; @@ -11,6 +13,7 @@ [Route("api")] public class GetAllErrorsController(IErrorMessageDataStore store) : ControllerBase { + [Authorize(Policy = Permissions.ErrorMessagesView)] [Route("errors")] [HttpGet] public async Task> ErrorsGet([FromQuery] PagingInfo pagingInfo, [FromQuery] SortInfo sortInfo, string status, string modified, string queueAddress) @@ -28,6 +31,7 @@ public async Task> ErrorsGet([FromQuery] PagingInfo pag return results.Results; } + [Authorize(Policy = Permissions.ErrorMessagesView)] [Route("errors")] [HttpHead] public async Task ErrorsHead(string status, string modified, string queueAddress) @@ -41,6 +45,7 @@ public async Task ErrorsHead(string status, string modified, string queueAddress Response.WithQueryStatsInfo(queryResult); } + [Authorize(Policy = Permissions.ErrorMessagesView)] [Route("endpoints/{endpointname}/errors")] [HttpGet] public async Task> ErrorsByEndpointName([FromQuery] PagingInfo pagingInfo, [FromQuery] SortInfo sortInfo, string status, string modified, string endpointName) @@ -58,6 +63,7 @@ public async Task> ErrorsByEndpointName([FromQuery] Pag return results.Results; } + [Authorize(Policy = Permissions.ErrorMessagesView)] [Route("errors/summary")] [HttpGet] public async Task> ErrorsSummary() => await store.ErrorsSummary(); diff --git a/src/ServiceControl/MessageFailures/Api/GetErrorByIdController.cs b/src/ServiceControl/MessageFailures/Api/GetErrorByIdController.cs index 437b6fc5a3..bb419a014c 100644 --- a/src/ServiceControl/MessageFailures/Api/GetErrorByIdController.cs +++ b/src/ServiceControl/MessageFailures/Api/GetErrorByIdController.cs @@ -1,6 +1,8 @@ namespace ServiceControl.MessageFailures.Api { using System.Threading.Tasks; + using Infrastructure.Auth; + using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Mvc; using Persistence; @@ -8,6 +10,7 @@ [Route("api")] public class GetErrorByIdController(IErrorMessageDataStore store) : ControllerBase { + [Authorize(Policy = Permissions.ErrorMessagesView)] [Route("errors/{failedMessageId:required:minlength(1)}")] [HttpGet] public async Task> ErrorBy(string failedMessageId) @@ -17,6 +20,7 @@ public async Task> ErrorBy(string failedMessageId) return result == null ? NotFound() : result; } + [Authorize(Policy = Permissions.ErrorMessagesView)] [Route("errors/last/{failedMessageId:required:minlength(1)}")] [HttpGet] public async Task> ErrorLastBy(string failedMessageId) diff --git a/src/ServiceControl/MessageFailures/Api/PendingRetryMessagesController.cs b/src/ServiceControl/MessageFailures/Api/PendingRetryMessagesController.cs index 611ee01e5c..1ad75d3bee 100644 --- a/src/ServiceControl/MessageFailures/Api/PendingRetryMessagesController.cs +++ b/src/ServiceControl/MessageFailures/Api/PendingRetryMessagesController.cs @@ -5,7 +5,9 @@ using System.Linq; using System.Text.Json.Serialization; using System.Threading.Tasks; + using Infrastructure.Auth; using InternalMessages; + using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Mvc; using NServiceBus; @@ -13,6 +15,7 @@ [Route("api")] public class PendingRetryMessagesController(IMessageSession session) : ControllerBase { + [Authorize(Policy = Permissions.ErrorMessagesRetry)] [Route("pendingretries/retry")] [HttpPost] public async Task RetryBy(string[] ids) @@ -28,6 +31,7 @@ public async Task RetryBy(string[] ids) return Accepted(); } + [Authorize(Policy = Permissions.ErrorMessagesRetry)] [Route("pendingretries/queues/retry")] [HttpPost] public async Task RetryBy(PendingRetryRequest request) diff --git a/src/ServiceControl/MessageFailures/Api/QueueAddressController.cs b/src/ServiceControl/MessageFailures/Api/QueueAddressController.cs index 9e01c66e15..44e043426c 100644 --- a/src/ServiceControl/MessageFailures/Api/QueueAddressController.cs +++ b/src/ServiceControl/MessageFailures/Api/QueueAddressController.cs @@ -2,7 +2,9 @@ { using System.Collections.Generic; using System.Threading.Tasks; + using Infrastructure.Auth; using Infrastructure.WebApi; + using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Mvc; using Persistence.Infrastructure; using ServiceControl.Persistence; @@ -11,6 +13,7 @@ [Route("api")] public class QueueAddressController(IQueueAddressStore store) : ControllerBase { + [Authorize(Policy = Permissions.ErrorQueuesView)] [Route("errors/queues/addresses")] [HttpGet] public async Task> GetAddresses([FromQuery] PagingInfo pagingInfo) @@ -22,6 +25,7 @@ public async Task> GetAddresses([FromQuery] PagingInfo pagin return result.Results; } + [Authorize(Policy = Permissions.ErrorQueuesView)] [Route("errors/queues/addresses/search/{search}")] [HttpGet] public async Task>> GetAddressesBySearchTerm([FromQuery] PagingInfo pagingInfo, string search = null) diff --git a/src/ServiceControl/MessageFailures/Api/ResolveMessagesController.cs b/src/ServiceControl/MessageFailures/Api/ResolveMessagesController.cs index dc4eddf431..21c05d2cc1 100644 --- a/src/ServiceControl/MessageFailures/Api/ResolveMessagesController.cs +++ b/src/ServiceControl/MessageFailures/Api/ResolveMessagesController.cs @@ -8,7 +8,9 @@ namespace ServiceControl.MessageFailures.Api using System.Linq; using System.Text.Json.Serialization; using System.Threading.Tasks; + using Infrastructure.Auth; using InternalMessages; + using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.Mvc.ModelBinding; using NServiceBus; @@ -17,6 +19,7 @@ namespace ServiceControl.MessageFailures.Api [Route("api")] public class ResolveMessagesController(IMessageSession session) : ControllerBase { + [Authorize(Policy = Permissions.ErrorMessagesRetry)] [Route("pendingretries/resolve")] [HttpPatch] public async Task ResolveBy(UniqueMessageIdsModel request) @@ -61,6 +64,7 @@ await session.SendLocal(m => return Accepted(); } + [Authorize(Policy = Permissions.ErrorMessagesRetry)] [Route("pendingretries/queues/resolve")] [HttpPatch] public async Task ResolveBy(QueueModel queueModel) diff --git a/src/ServiceControl/MessageFailures/Api/RetryMessagesController.cs b/src/ServiceControl/MessageFailures/Api/RetryMessagesController.cs index c486129df9..29fb12966c 100644 --- a/src/ServiceControl/MessageFailures/Api/RetryMessagesController.cs +++ b/src/ServiceControl/MessageFailures/Api/RetryMessagesController.cs @@ -4,7 +4,9 @@ using System.Linq; using System.Net.Http; using System.Threading.Tasks; + using Infrastructure.Auth; using InternalMessages; + using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Http.Extensions; using Microsoft.AspNetCore.Mvc; @@ -23,6 +25,7 @@ public class RetryMessagesController( IMessageSession messageSession, ILogger logger) : ControllerBase { + [Authorize(Policy = Permissions.ErrorMessagesRetry)] [Route("errors/{failedMessageId:required:minlength(1)}/retry")] [HttpPost] public async Task RetryMessageBy([FromQuery(Name = "instance_id")] string instanceId, string failedMessageId) @@ -49,6 +52,7 @@ public async Task RetryMessageBy([FromQuery(Name = "instance_id") return Empty; } + [Authorize(Policy = Permissions.ErrorMessagesRetry)] [Route("errors/retry")] [HttpPost] public async Task RetryAllBy(List messageIds) @@ -63,6 +67,7 @@ public async Task RetryAllBy(List messageIds) return Accepted(); } + [Authorize(Policy = Permissions.ErrorMessagesRetry)] [Route("errors/queues/{queueAddress:required:minlength(1)}/retry")] [HttpPost] public async Task RetryAllBy(string queueAddress) @@ -76,6 +81,7 @@ await messageSession.SendLocal(m => return Accepted(); } + [Authorize(Policy = Permissions.ErrorMessagesRetry)] [Route("errors/retry/all")] [HttpPost] public async Task RetryAll() @@ -85,6 +91,7 @@ public async Task RetryAll() return Accepted(); } + [Authorize(Policy = Permissions.ErrorMessagesRetry)] [Route("errors/{endpointName:required:minlength(1)}/retry/all")] [HttpPost] public async Task RetryAllByEndpoint(string endpointName) diff --git a/src/ServiceControl/MessageFailures/Api/UnArchiveMessagesController.cs b/src/ServiceControl/MessageFailures/Api/UnArchiveMessagesController.cs index 0a0271d9fa..d36940bc99 100644 --- a/src/ServiceControl/MessageFailures/Api/UnArchiveMessagesController.cs +++ b/src/ServiceControl/MessageFailures/Api/UnArchiveMessagesController.cs @@ -4,7 +4,9 @@ using System.Globalization; using System.Linq; using System.Threading.Tasks; + using Infrastructure.Auth; using InternalMessages; + using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Mvc; using NServiceBus; @@ -12,6 +14,7 @@ [Route("api")] public class UnArchiveMessagesController(IMessageSession session) : ControllerBase { + [Authorize(Policy = Permissions.ErrorMessagesUnarchive)] [Route("errors/unarchive")] [HttpPatch] public async Task Unarchive(string[] ids) @@ -28,6 +31,7 @@ public async Task Unarchive(string[] ids) return Accepted(); } + [Authorize(Policy = Permissions.ErrorMessagesUnarchive)] [Route("errors/{from}...{to}/unarchive")] [HttpPatch] public async Task Unarchive(string from, string to) diff --git a/src/ServiceControl/MessageRedirects/Api/MessageRedirectsController.cs b/src/ServiceControl/MessageRedirects/Api/MessageRedirectsController.cs index 4899498683..2eed6206cc 100644 --- a/src/ServiceControl/MessageRedirects/Api/MessageRedirectsController.cs +++ b/src/ServiceControl/MessageRedirects/Api/MessageRedirectsController.cs @@ -7,9 +7,11 @@ using System.Text.Json.Serialization; using System.Threading.Tasks; using Contracts.MessageRedirects; + using Infrastructure.Auth; using Infrastructure.DomainEvents; using Infrastructure.WebApi; using MessageFailures.InternalMessages; + using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Mvc; using NServiceBus; using ServiceControl.Persistence.Infrastructure; @@ -25,6 +27,7 @@ public class MessageRedirectsController( IDomainEvents events) : ControllerBase { + [Authorize(Policy = Permissions.ErrorRedirectsManage)] [Route("redirects")] [HttpPost] public async Task NewRedirects(MessageRedirectRequest request) @@ -93,6 +96,7 @@ await session.SendLocal(new RetryPendingMessages return StatusCode((int)HttpStatusCode.Created, messageRedirect); } + [Authorize(Policy = Permissions.ErrorRedirectsManage)] [Route("redirects/{messageRedirectId:guid}")] [HttpPut] public async Task UpdateRedirect(Guid messageRedirectId, MessageRedirectRequest request) @@ -135,6 +139,7 @@ public async Task UpdateRedirect(Guid messageRedirectId, MessageR return NoContent(); } + [Authorize(Policy = Permissions.ErrorRedirectsManage)] [Route("redirects/{messageRedirectId:guid}")] [HttpDelete] public async Task DeleteRedirect(Guid messageRedirectId) @@ -162,6 +167,7 @@ await events.Raise(new MessageRedirectRemoved return NoContent(); } + [Authorize(Policy = Permissions.ErrorRedirectsView)] [Route("redirect")] [HttpHead] public async Task CountRedirects() @@ -172,6 +178,7 @@ public async Task CountRedirects() Response.WithTotalCount(redirects.Redirects.Count); } + [Authorize(Policy = Permissions.ErrorRedirectsView)] [Route("redirects")] [HttpGet] public async Task> Redirects(string sort, string direction, [FromQuery] PagingInfo pagingInfo) diff --git a/src/ServiceControl/Monitoring/Web/EndpointsMonitoringController.cs b/src/ServiceControl/Monitoring/Web/EndpointsMonitoringController.cs index 5b64d5a22d..0926075b59 100644 --- a/src/ServiceControl/Monitoring/Web/EndpointsMonitoringController.cs +++ b/src/ServiceControl/Monitoring/Web/EndpointsMonitoringController.cs @@ -4,6 +4,7 @@ using System.Collections.Generic; using System.Threading.Tasks; using CompositeViews.Messages; + using Infrastructure.Auth; using Infrastructure.WebApi; using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Http.Extensions; @@ -25,10 +26,12 @@ public class EndpointsMonitoringController( IMonitoringDataStore dataStore) : ControllerBase { + [Authorize(Policy = Permissions.ErrorHeartbeatsView)] [Route("heartbeats/stats")] [HttpGet] public EndpointMonitoringStats HeartbeatStats() => monitoring.GetStats(); + [Authorize(Policy = Permissions.ErrorEndpointsView)] [Route("endpoints")] [HttpGet] public EndpointsView[] Endpoints() => monitoring.GetEndpoints(); @@ -44,6 +47,7 @@ public void GetSupportedOperations() Response.Headers.AccessControlExposeHeaders = "Allow"; } + [Authorize(Policy = Permissions.ErrorEndpointsDelete)] [Route("endpoints/{endpointId}")] [HttpDelete] public async Task DeleteEndpoint(Guid endpointId) @@ -59,6 +63,7 @@ public async Task DeleteEndpoint(Guid endpointId) return NoContent(); } + [Authorize(Policy = Permissions.ErrorEndpointsView)] [Route("endpoints/known")] [HttpGet] public async Task> KnownEndpoints([FromQuery] PagingInfo pagingInfo) @@ -70,6 +75,7 @@ public async Task> KnownEndpoints([FromQuery] PagingIn return result.Results; } + [Authorize(Policy = Permissions.ErrorEndpointsManage)] [Route("endpoints/{endpointId}")] [HttpPatch] public async Task Monitoring(Guid endpointId, [FromBody] EndpointUpdateModel data) diff --git a/src/ServiceControl/Monitoring/Web/EndpointsSettingsController.cs b/src/ServiceControl/Monitoring/Web/EndpointsSettingsController.cs index be1807e54c..a1d55138db 100644 --- a/src/ServiceControl/Monitoring/Web/EndpointsSettingsController.cs +++ b/src/ServiceControl/Monitoring/Web/EndpointsSettingsController.cs @@ -4,6 +4,8 @@ using System.Runtime.CompilerServices; using System.Threading; using System.Threading.Tasks; +using Infrastructure.Auth; +using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Mvc; using Persistence; using ServiceBus.Management.Infrastructure.Settings; @@ -25,6 +27,7 @@ public class EndpointsSettingsController( IEndpointSettingsStore dataStore, Settings settings) : ControllerBase { + [Authorize(Policy = Permissions.ErrorEndpointsView)] [Route("endpointssettings")] [HttpGet] public async IAsyncEnumerable Endpoints([EnumeratorCancellation] CancellationToken token) @@ -49,6 +52,7 @@ public async IAsyncEnumerable Endpoints([EnumeratorCancellation] C } } + [Authorize(Policy = Permissions.ErrorEndpointsManage)] [Route("endpointssettings/{endpointName?}")] [HttpPatch] public async Task diff --git a/src/ServiceControl/Notifications/Api/NotificationsController.cs b/src/ServiceControl/Notifications/Api/NotificationsController.cs index af5f90cfe0..e42cfa5473 100644 --- a/src/ServiceControl/Notifications/Api/NotificationsController.cs +++ b/src/ServiceControl/Notifications/Api/NotificationsController.cs @@ -4,6 +4,8 @@ using System.Net; using System.Threading.Tasks; using Email; + using Infrastructure.Auth; + using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Mvc; using Persistence; using ServiceBus.Management.Infrastructure.Settings; @@ -12,6 +14,7 @@ [Route("api")] public class NotificationsController(IErrorMessageDataStore store, Settings settings, EmailSender emailSender) : ControllerBase { + [Authorize(Policy = Permissions.ErrorNotificationsView)] [Route("notifications/email")] [HttpGet] public async Task GetEmailNotificationsSettings() @@ -22,6 +25,7 @@ public async Task GetEmailNotificationsSettings() return notificationsSettings.Email; } + [Authorize(Policy = Permissions.ErrorNotificationsManage)] [Route("notifications/email/toggle")] [HttpPost] public async Task ToggleEmailNotifications(ToggleEmailNotifications request) @@ -36,6 +40,7 @@ public async Task ToggleEmailNotifications(ToggleEmailNotificatio return Ok(); } + [Authorize(Policy = Permissions.ErrorNotificationsManage)] [Route("notifications/email")] [HttpPost] public async Task UpdateSettings(UpdateEmailNotificationsSettingsRequest request) @@ -60,6 +65,7 @@ public async Task UpdateSettings(UpdateEmailNotificationsSettings return Ok(); } + [Authorize(Policy = Permissions.ErrorNotificationsTest)] [Route("notifications/email/test")] [HttpPost] public async Task SendTestEmail() diff --git a/src/ServiceControl/Recoverability/API/FailureGroupsArchiveController.cs b/src/ServiceControl/Recoverability/API/FailureGroupsArchiveController.cs index f40611db85..69c805f199 100644 --- a/src/ServiceControl/Recoverability/API/FailureGroupsArchiveController.cs +++ b/src/ServiceControl/Recoverability/API/FailureGroupsArchiveController.cs @@ -1,6 +1,8 @@ namespace ServiceControl.Recoverability.API { using System.Threading.Tasks; + using Infrastructure.Auth; + using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Mvc; using NServiceBus; using ServiceControl.Persistence.Recoverability; @@ -9,6 +11,7 @@ [Route("api")] public class FailureGroupsArchiveController(IMessageSession bus, IArchiveMessages archiver) : ControllerBase { + [Authorize(Policy = Permissions.ErrorRecoverabilityGroupsArchive)] [Route("recoverability/groups/{groupId:required:minlength(1)}/errors/archive")] [HttpPost] public async Task ArchiveGroupErrors(string groupId) diff --git a/src/ServiceControl/Recoverability/API/FailureGroupsController.cs b/src/ServiceControl/Recoverability/API/FailureGroupsController.cs index b03b2b702d..7a3964f9cb 100644 --- a/src/ServiceControl/Recoverability/API/FailureGroupsController.cs +++ b/src/ServiceControl/Recoverability/API/FailureGroupsController.cs @@ -3,8 +3,10 @@ using System.Collections.Generic; using System.Linq; using System.Threading.Tasks; + using Infrastructure.Auth; using Infrastructure.WebApi; using MessageFailures.Api; + using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Mvc; using Persistence.Infrastructure; using ServiceControl.Persistence; @@ -18,6 +20,7 @@ public class FailureGroupsController( IRetryHistoryDataStore retryStore) : ControllerBase { + [Authorize(Policy = Permissions.ErrorRecoverabilityGroupsView)] [Route("recoverability/classifiers")] [HttpGet] public string[] GetSupportedClassifiers() @@ -32,6 +35,7 @@ public string[] GetSupportedClassifiers() return result; } + [Authorize(Policy = Permissions.ErrorRecoverabilityGroupsView)] [Route("recoverability/groups/{groupId:required:minlength(1)}/comment")] [HttpPost] public async Task EditComment(string groupId, string comment) @@ -41,6 +45,7 @@ public async Task EditComment(string groupId, string comment) return Accepted(); } + [Authorize(Policy = Permissions.ErrorRecoverabilityGroupsView)] [Route("recoverability/groups/{groupId:required:minlength(1)}/comment")] [HttpDelete] public async Task DeleteComment(string groupId) @@ -50,6 +55,7 @@ public async Task DeleteComment(string groupId) return Accepted(); } + [Authorize(Policy = Permissions.ErrorRecoverabilityGroupsView)] [Route("recoverability/groups/{classifier?}")] [HttpGet] public async Task GetAllGroups(string classifier = "Exception Type and Stack Trace", string classifierFilter = default) @@ -64,6 +70,7 @@ public async Task GetAllGroups(string classifier = "Exception return results; } + [Authorize(Policy = Permissions.ErrorRecoverabilityGroupsView)] [Route("recoverability/groups/{groupId:required:minlength(1)}/errors")] [HttpGet] public async Task> GetGroupErrors(string groupId, [FromQuery] SortInfo sortInfo, [FromQuery] PagingInfo pagingInfo, string status = default, string modified = default) @@ -75,6 +82,7 @@ public async Task> GetGroupErrors(string groupId, [From } + [Authorize(Policy = Permissions.ErrorRecoverabilityGroupsView)] [Route("recoverability/groups/{groupId:required:minlength(1)}/errors")] [HttpHead] public async Task GetGroupErrorsCount(string groupId, string status = default, string modified = default) @@ -84,6 +92,7 @@ public async Task GetGroupErrorsCount(string groupId, string status = default, s Response.WithQueryStatsInfo(results); } + [Authorize(Policy = Permissions.ErrorRecoverabilityGroupsView)] [Route("recoverability/history")] [HttpGet] public async Task GetRetryHistory() @@ -95,6 +104,7 @@ public async Task GetRetryHistory() return retryHistory; } + [Authorize(Policy = Permissions.ErrorRecoverabilityGroupsView)] [Route("recoverability/groups/id/{groupId:required:minlength(1)}")] [HttpGet] public async Task GetGroup(string groupId, string status = default, string modified = default) diff --git a/src/ServiceControl/Recoverability/API/FailureGroupsRetryController.cs b/src/ServiceControl/Recoverability/API/FailureGroupsRetryController.cs index 308d427217..8ef2ec86d2 100644 --- a/src/ServiceControl/Recoverability/API/FailureGroupsRetryController.cs +++ b/src/ServiceControl/Recoverability/API/FailureGroupsRetryController.cs @@ -2,6 +2,8 @@ namespace ServiceControl.Recoverability.API { using System; using System.Threading.Tasks; + using Infrastructure.Auth; + using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Mvc; using NServiceBus; using ServiceControl.Persistence; @@ -10,6 +12,7 @@ namespace ServiceControl.Recoverability.API [Route("api")] public class FailureGroupsRetryController(IMessageSession bus, RetryingManager retryingManager) : ControllerBase { + [Authorize(Policy = Permissions.ErrorRecoverabilityGroupsRetry)] [Route("recoverability/groups/{groupId:required:minlength(1)}/errors/retry")] [HttpPost] public async Task ArchiveGroupErrors(string groupId) diff --git a/src/ServiceControl/Recoverability/API/FailureGroupsUnarchiveController.cs b/src/ServiceControl/Recoverability/API/FailureGroupsUnarchiveController.cs index 5661c8dd78..37e2f0b6d9 100644 --- a/src/ServiceControl/Recoverability/API/FailureGroupsUnarchiveController.cs +++ b/src/ServiceControl/Recoverability/API/FailureGroupsUnarchiveController.cs @@ -1,6 +1,8 @@ namespace ServiceControl.Recoverability.API { using System.Threading.Tasks; + using Infrastructure.Auth; + using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Mvc; using NServiceBus; using ServiceControl.Persistence.Recoverability; @@ -9,6 +11,7 @@ [Route("api")] public class FailureGroupsUnarchiveController(IMessageSession bus, IArchiveMessages archiver) : ControllerBase { + [Authorize(Policy = Permissions.ErrorRecoverabilityGroupsUnarchive)] [Route("recoverability/groups/{groupId:required:minlength(1)}/errors/unarchive")] [HttpPost] public async Task UnarchiveGroupErrors(string groupId) diff --git a/src/ServiceControl/Recoverability/API/UnacknowledgedGroupsController.cs b/src/ServiceControl/Recoverability/API/UnacknowledgedGroupsController.cs index 2e85be691f..9be6bec2b2 100644 --- a/src/ServiceControl/Recoverability/API/UnacknowledgedGroupsController.cs +++ b/src/ServiceControl/Recoverability/API/UnacknowledgedGroupsController.cs @@ -1,6 +1,8 @@ namespace ServiceControl.Recoverability.API { using System.Threading.Tasks; + using Infrastructure.Auth; + using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Mvc; using ServiceControl.Persistence; using ServiceControl.Persistence.Recoverability; @@ -9,6 +11,7 @@ [Route("api")] public class UnacknowledgedGroupsController(IRetryHistoryDataStore retryStore, IArchiveMessages archiver) : ControllerBase { + [Authorize(Policy = Permissions.ErrorRecoverabilityGroupsView)] [Route("recoverability/unacknowledgedgroups/{groupId:required:minlength(1)}")] [HttpDelete] public async Task AcknowledgeOperation(string groupId) diff --git a/src/ServiceControl/SagaAudit/SagasController.cs b/src/ServiceControl/SagaAudit/SagasController.cs index 9b581b9758..d3c082de8f 100644 --- a/src/ServiceControl/SagaAudit/SagasController.cs +++ b/src/ServiceControl/SagaAudit/SagasController.cs @@ -2,7 +2,9 @@ namespace ServiceControl.SagaAudit { using System; using System.Threading.Tasks; + using Infrastructure.Auth; using Infrastructure.WebApi; + using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Http.Extensions; using Microsoft.AspNetCore.Mvc; using Persistence.Infrastructure; @@ -11,6 +13,7 @@ namespace ServiceControl.SagaAudit [Route("api")] public class SagasController(GetSagaByIdApi getSagaByIdApi) : ControllerBase { + [Authorize(Policy = Permissions.ErrorSagasView)] [Route("sagas/{id}")] [HttpGet] public async Task Sagas([FromQuery] PagingInfo pagingInfo, Guid id)