Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@
<ItemGroup>
<ProjectReference Include="..\Particular.LicensingComponent.Persistence\Particular.LicensingComponent.Persistence.csproj" />
<ProjectReference Include="..\ServiceControl.Api\ServiceControl.Api.csproj" />
<ProjectReference Include="..\ServiceControl.Infrastructure\ServiceControl.Infrastructure.csproj" />
<ProjectReference Include="..\ServiceControl.Transports\ServiceControl.Transports.csproj" />
</ItemGroup>

Expand Down
10 changes: 10 additions & 0 deletions src/Particular.LicensingComponent/WebApi/LicensingController.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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")]
Expand All @@ -19,13 +21,15 @@ public LicensingController(IThroughputCollector throughputCollector)
this.throughputCollector = throughputCollector;
}

[Authorize(Policy = Permissions.ErrorThroughputView)]
[Route("endpoints")]
[HttpGet]
public async Task<List<EndpointThroughputSummary>> GetEndpointThroughput(CancellationToken cancellationToken)
{
return await throughputCollector.GetThroughputSummary(cancellationToken);
}

[Authorize(Policy = Permissions.ErrorThroughputManage)]
[Route("endpoints/update")]
[HttpPost]
public async Task<IActionResult> UpdateUserSelectionOnEndpointThroughput(List<UpdateUserIndicator> updateUserIndicators, CancellationToken cancellationToken)
Expand All @@ -34,13 +38,15 @@ public async Task<IActionResult> UpdateUserSelectionOnEndpointThroughput(List<Up
return Ok();
}

[Authorize(Policy = Permissions.ErrorThroughputView)]
[Route("report/available")]
[HttpGet]
public async Task<ReportGenerationState> CanThroughputReportBeGenerated(CancellationToken cancellationToken)
{
return await throughputCollector.GetReportGenerationState(cancellationToken);
}

[Authorize(Policy = Permissions.ErrorThroughputView)]
[Route("report/file")]
[HttpGet]
public async Task GetThroughputReportFile([FromQuery(Name = "spVersion")] string? spVersion, CancellationToken cancellationToken)
Expand Down Expand Up @@ -77,24 +83,28 @@ 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<ThroughputConnectionSettings> GetThroughputSettingsInformation(CancellationToken cancellationToken)
{
return await throughputCollector.GetThroughputConnectionSettingsInformation(cancellationToken);
}

[Authorize(Policy = Permissions.ErrorThroughputView)]
[Route("settings/test")]
[HttpGet]
public async Task<ConnectionTestResults> TestThroughputConnectionSettings(CancellationToken cancellationToken) => await throughputCollector.TestConnectionSettings(cancellationToken);

[Authorize(Policy = Permissions.ErrorThroughputView)]
[Route("settings/masks")]
[HttpGet]
public async Task<List<string>> GetMasks(CancellationToken cancellationToken)
{
return await throughputCollector.GetReportMasks(cancellationToken);
}

[Authorize(Policy = Permissions.ErrorThroughputManage)]
[Route("settings/masks/update")]
[HttpPost]
public async Task<IActionResult> UpdateMasks(List<string> updateMasks, CancellationToken cancellationToken)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,18 @@ public OpenIdConnectTestConfiguration WithAuthenticationDisabled()
return this;
}

/// <summary>
/// Enables role-based authorization. When on, controllers carrying
/// <c>[Authorize(Policy = Permissions.X)]</c> require the caller's "roles" claim to map to a
/// role that grants the permission via <c>RolePermissions</c>. When off, the policy provider
/// returns allow-all policies and any authenticated request reaches the controller.
/// </summary>
public OpenIdConnectTestConfiguration WithRoleBasedAuthorizationEnabled()
{
SetEnvironmentVariable("AUTHENTICATION_ROLEBASEDAUTHORIZATIONENABLED", "true");
return this;
}

/// <summary>
/// Disables settings validation. This allows testing with placeholder/fake OIDC settings.
/// Should only be used in test scenarios where a real OIDC provider is not available.
Expand Down Expand Up @@ -164,6 +176,7 @@ public void ClearConfiguration()
ClearEnvironmentVariable("AUTHENTICATION_SERVICEPULSE_CLIENTID");
ClearEnvironmentVariable("AUTHENTICATION_SERVICEPULSE_APISCOPES");
ClearEnvironmentVariable("AUTHENTICATION_SERVICEPULSE_AUTHORITY");
ClearEnvironmentVariable("AUTHENTICATION_ROLEBASEDAUTHORIZATIONENABLED");
ClearEnvironmentVariable("VALIDATECONFIG");
}

Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
namespace ServiceControl.AcceptanceTests.Security.OpenIdConnect
{
using System.Net.Http;
using System.Security.Claims;
using System.Threading.Tasks;
using AcceptanceTesting;
using AcceptanceTesting.OpenIdConnect;
Expand Down Expand Up @@ -35,6 +36,7 @@ public void ConfigureAuth()
configuration = new OpenIdConnectTestConfiguration(ServiceControlInstanceType.Primary)
.WithConfigurationValidationDisabled()
.WithAuthenticationEnabled()
.WithRoleBasedAuthorizationEnabled()
.WithAuthority(mockOidcServer.Authority)
.WithAudience(TestAudience)
.WithServicePulseClientId(TestClientId)
Expand Down Expand Up @@ -124,7 +126,10 @@ public async Task Should_accept_requests_with_valid_bearer_token()
_ = await Define<Context>()
.Done(async ctx =>
{
var validToken = mockOidcServer.GenerateToken();
// The "reader" role grants every *:*:view permission, including error:messages:view
// required by /api/errors. Without a role-bearing claim the request would be 403.
var validToken = mockOidcServer.GenerateToken(
additionalClaims: new[] { new Claim("roles", "reader") });
response = await OpenIdConnectAssertions.SendRequestWithBearerToken(
HttpClient,
HttpMethod.Get,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -123,6 +123,7 @@ async Task InitializeServiceControl(ScenarioContext context)
EnvironmentName = Environments.Development
});
hostBuilder.AddServiceControlAuthentication(settings.OpenIdConnectSettings);
hostBuilder.AddServiceControlAuthorization(settings.OpenIdConnectSettings);
hostBuilder.AddServiceControl(settings, configuration);
hostBuilder.AddServiceControlHttps(settings.HttpsSettings);
hostBuilder.AddServiceControlApi(settings.CorsSettings);
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
namespace ServiceControl.Audit.AcceptanceTests.Security.OpenIdConnect
{
using System.Net.Http;
using System.Security.Claims;
using System.Threading.Tasks;
using AcceptanceTesting;
using AcceptanceTesting.OpenIdConnect;
Expand Down Expand Up @@ -32,6 +33,7 @@ public void ConfigureAuth()
configuration = new OpenIdConnectTestConfiguration(ServiceControlInstanceType.Audit)
.WithConfigurationValidationDisabled()
.WithAuthenticationEnabled()
.WithRoleBasedAuthorizationEnabled()
.WithAuthority(mockOidcServer.Authority)
.WithAudience(TestAudience)
.WithRequireHttpsMetadata(false);
Expand Down Expand Up @@ -92,7 +94,10 @@ public async Task Should_accept_requests_with_valid_bearer_token()
_ = await Define<Context>()
.Done(async ctx =>
{
var validToken = mockOidcServer.GenerateToken();
// The "reader" role grants every *:*:view permission, including audit:message:view
// required by /api/messages. Without a role-bearing claim the request would be 403.
var validToken = mockOidcServer.GenerateToken(
additionalClaims: new[] { new Claim("roles", "reader") });
response = await OpenIdConnectAssertions.SendRequestWithBearerToken(
HttpClient,
HttpMethod.Get,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -120,6 +120,7 @@ async Task InitializeServiceControl(ScenarioContext context)
EnvironmentName = Environments.Development
});
hostBuilder.AddServiceControlAuthentication(settings.OpenIdConnectSettings);
hostBuilder.AddServiceControlAuthorization(settings.OpenIdConnectSettings);
hostBuilder.AddServiceControlAudit((criticalErrorContext, cancellationToken) =>
{
var logitem = new ScenarioContext.LogItem
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,9 @@
"RequireHttpsMetadata": true,
"ServicePulseAuthority": null,
"ServicePulseClientId": null,
"ServicePulseApiScopes": null
"ServicePulseApiScopes": null,
"RolesClaim": "roles",
"RoleBasedAuthorizationEnabled": false
},
"ForwardedHeadersSettings": {
"Enabled": true,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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<IList<MessagesView>> GetAllMessages(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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<IList<MessagesView>> GetAllMessages([FromQuery] PagingInfo pagingInfo, [FromQuery] SortInfo sortInfo, [FromQuery(Name = "include_system_messages")] bool includeSystemMessages, CancellationToken cancellationToken)
Expand All @@ -23,6 +25,7 @@ public async Task<IList<MessagesView>> GetAllMessages([FromQuery] PagingInfo pag
return result.Results;
}

[Authorize(Policy = Permissions.AuditMessageView)]
[Route("endpoints/{endpoint}/messages")]
[HttpGet]
public async Task<IList<MessagesView>> GetEndpointMessages([FromQuery] PagingInfo pagingInfo, [FromQuery] SortInfo sortInfo, [FromQuery(Name = "include_system_messages")] bool includeSystemMessages, string endpoint, CancellationToken cancellationToken)
Expand All @@ -43,6 +46,7 @@ public async Task<IList<AuditCount>> GetEndpointAuditCounts([FromQuery] PagingIn
return result.Results;
}

[Authorize(Policy = Permissions.AuditMessageView)]
[Route("messages/{id}/body")]
[HttpGet]
public async Task<IActionResult> Get(string id, CancellationToken cancellationToken)
Expand All @@ -69,6 +73,7 @@ public async Task<IActionResult> 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<IList<MessagesView>> Search([FromQuery] PagingInfo pagingInfo, [FromQuery] SortInfo sortInfo, string q, CancellationToken cancellationToken)
Expand All @@ -78,6 +83,7 @@ public async Task<IList<MessagesView>> Search([FromQuery] PagingInfo pagingInfo,
return result.Results;
}

[Authorize(Policy = Permissions.AuditMessageView)]
[Route("messages/search/{keyword}")]
[HttpGet]
public async Task<IList<MessagesView>> SearchByKeyWord([FromQuery] PagingInfo pagingInfo, [FromQuery] SortInfo sortInfo, string keyword, CancellationToken cancellationToken)
Expand All @@ -87,6 +93,7 @@ public async Task<IList<MessagesView>> SearchByKeyWord([FromQuery] PagingInfo pa
return result.Results;
}

[Authorize(Policy = Permissions.AuditMessageView)]
[Route("endpoints/{endpoint}/messages/search")]
[HttpGet]
public async Task<IList<MessagesView>> Search([FromQuery] PagingInfo pagingInfo, [FromQuery] SortInfo sortInfo, string endpoint, string q, CancellationToken cancellationToken)
Expand All @@ -96,6 +103,7 @@ public async Task<IList<MessagesView>> Search([FromQuery] PagingInfo pagingInfo,
return result.Results;
}

[Authorize(Policy = Permissions.AuditMessageView)]
[Route("endpoints/{endpoint}/messages/search/{keyword}")]
[HttpGet]
public async Task<IList<MessagesView>> SearchByKeyword([FromQuery] PagingInfo pagingInfo, [FromQuery] SortInfo sortInfo, string endpoint, string keyword, CancellationToken cancellationToken)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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<IList<MessagesView>> Get([FromQuery] PagingInfo pagingInfo, [FromQuery] SortInfo sortInfo, string conversationId, CancellationToken cancellationToken)
Expand Down
3 changes: 3 additions & 0 deletions src/ServiceControl.Audit/Connection/ConnectionController.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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")]
Expand All @@ -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() =>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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);
hostBuilder.AddServiceControlHttps(settings.HttpsSettings);
hostBuilder.AddServiceControlAudit((_, __) =>
{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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<IList<KnownEndpointsView>> GetAll([FromQuery] PagingInfo pagingInfo, CancellationToken cancellationToken)
Expand Down
3 changes: 3 additions & 0 deletions src/ServiceControl.Audit/SagaAudit/SagasController.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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<SagaHistory> Sagas([FromQuery] PagingInfo pagingInfo, Guid id, CancellationToken cancellationToken)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
using System;
using System.Text.Json;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Authentication;
using Microsoft.AspNetCore.Authentication.JwtBearer;
using Microsoft.AspNetCore.Http;
using Microsoft.Extensions.DependencyInjection;
Expand Down Expand Up @@ -36,7 +37,8 @@ public static void AddServiceControlAuthentication(this IHostApplicationBuilder
ValidateLifetime = oidcSettings.ValidateLifetime,
ValidateIssuerSigningKey = oidcSettings.ValidateIssuerSigningKey,
ValidAudience = oidcSettings.Audience,
ClockSkew = TimeSpan.FromMinutes(5) // Allow 5 minutes clock skew
ClockSkew = TimeSpan.FromMinutes(5), // Allow 5 minutes clock skew
RoleClaimType = oidcSettings.RolesClaim
};
options.RequireHttpsMetadata = oidcSettings.RequireHttpsMetadata;
// Don't map inbound claims to legacy Microsoft claim types
Expand Down Expand Up @@ -99,6 +101,12 @@ public static void AddServiceControlAuthentication(this IHostApplicationBuilder
configure.FallbackPolicy = new Microsoft.AspNetCore.Authorization.AuthorizationPolicyBuilder()
.RequireAuthenticatedUser()
.Build());

// Normalise per-IdP role claim shapes (Keycloak's nested realm_access.roles, Entra app
// roles, Cognito groups) into canonical "roles" claims for the verb handler. The source
// path is configurable via Authentication.RolesClaim.
hostBuilder.Services.AddSingleton<IClaimsTransformation>(
new RolesClaimsTransformation(oidcSettings.RolesClaim));
}

static string GetErrorMessage(JwtBearerChallengeContext context)
Expand Down
Loading
Loading