Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

added authorization requirement to endpoints #272

Merged
merged 16 commits into from
Oct 19, 2023
Merged
Show file tree
Hide file tree
Changes from 10 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
1 change: 0 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -227,7 +227,6 @@ ClientBin/
*.dbmdl
*.dbproj.schemaview
*.jfm
*.pfx
*.publishsettings
orleans.codegen.cs

Expand Down
2 changes: 2 additions & 0 deletions src/Altinn.Notifications/Altinn.Notifications.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,8 @@
</PropertyGroup>

<ItemGroup>
<PackageReference Include="Altinn.Common.AccessToken" Version="3.0.1" />
<PackageReference Include="Altinn.Common.PEP" Version="1.3.0" />
<PackageReference Include="FluentValidation" Version="11.7.1" />
<PackageReference Include="JWTCookieAuthentication" Version="3.0.1" />
<PackageReference Include="Microsoft.ApplicationInsights.AspNetCore" Version="2.21.0" />
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
using Altinn.Common.AccessToken;
using Altinn.Common.PEP.Authorization;

namespace Altinn.Notifications.Authorization;

/// <summary>
/// This requirement was created to allow access if either Scope or AccessToken verification is successful.
/// It inherits from both <see cref="IAccessTokenRequirement"/> and <see cref="IScopeAccessRequirement"/> which
/// will trigger both <see cref="AccessTokenHandler"/> and <see cref="ScopeAccessHandler"/>. If any of them
/// indicate success, authorization will succeed.
/// </summary>
public class CreateScopeOrAccessTokenRequirement : IAccessTokenRequirement, IScopeAccessRequirement
{
/// <summary>
/// Initializes a new instance of the <see cref="CreateScopeOrAccessTokenRequirement"/> class with the given scope.
/// </summary>
public CreateScopeOrAccessTokenRequirement(string scope)
{
ApprovedIssuers = Array.Empty<string>();
Scope = new string[] { scope };
}

/// <inheritdoc/>
public string[] ApprovedIssuers { get; set; }

/// <inheritdoc/>
public string[] Scope { get; set; }
}
18 changes: 18 additions & 0 deletions src/Altinn.Notifications/Configuration/AuthorizationConstants.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
namespace Altinn.Notifications.Configuration
{
/// <summary>
/// Constants related to authorization of notifications
/// </summary>
public static class AuthorizationConstants
{
/// <summary>
/// Id for the policy requiring create scope or access platform access token
/// </summary>
public const string POLICY_CREATE_SCOPE_OR_PLATFORM_ACCESS = "CreateScopeOrPlatfomAccessToken";

/// <summary>
/// Scope for allowing access to creating notifications
/// </summary>
public const string SCOPE_NOTIFICATIONS_CREATE = "altinn:notifications.create";
SandGrainOne marked this conversation as resolved.
Show resolved Hide resolved
}
}
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
using Altinn.Notifications.Core.Models;
using Altinn.Notifications.Configuration;
using Altinn.Notifications.Core.Models;
using Altinn.Notifications.Core.Models.Orders;
using Altinn.Notifications.Core.Services.Interfaces;
using Altinn.Notifications.Extensions;
Expand All @@ -21,7 +22,6 @@ namespace Altinn.Notifications.Controllers;
/// </summary>
[Route("notifications/api/v1/orders/email")]
[ApiController]
[Authorize]
[SwaggerResponse(401, "Caller is unauthorized")]
[SwaggerResponse(403, "Caller is not authorized to access the requested resource")]

Expand All @@ -45,6 +45,7 @@ public EmailNotificationOrdersController(IValidator<EmailNotificationOrderReques
/// The system will also attempt to verify that it will be possible to fulfill the order.
/// </summary>
/// <returns>The id of the registered notification order</returns>
[Authorize(Policy = AuthorizationConstants.POLICY_CREATE_SCOPE_OR_PLATFORM_ACCESS)]
[HttpPost]
[Consumes("application/json")]
[Produces("application/json")]
Expand All @@ -60,7 +61,7 @@ public async Task<ActionResult<OrderIdExt>> Post(EmailNotificationOrderRequestEx
return ValidationProblem(ModelState);
}

string? creator = User.GetOrg();
string? creator = HttpContext.Items["Org"] as string;

if (creator == null)
{
Expand Down
8 changes: 4 additions & 4 deletions src/Altinn.Notifications/Controllers/OrdersController.cs
acn-sbuad marked this conversation as resolved.
Show resolved Hide resolved
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
using Altinn.Notifications.Core.Services.Interfaces;
using Altinn.Notifications.Extensions;
using Altinn.Notifications.Mappers;
using Altinn.Notifications.Models;

Expand Down Expand Up @@ -43,7 +42,8 @@ public OrdersController(IGetOrderService getOrderService)
[SwaggerResponse(404, "No order with the provided id was found")]
public async Task<ActionResult<NotificationOrderExt>> GetById([FromRoute] Guid id)
{
string? expectedCreator = User.GetOrg();
string? expectedCreator = HttpContext.Items["Org"] as string;

if (expectedCreator == null)
{
return Forbid();
Expand All @@ -69,7 +69,7 @@ public async Task<ActionResult<NotificationOrderExt>> GetById([FromRoute] Guid i
[SwaggerResponse(200, "The list of notification orders matching the provided senders ref was retrieved successfully", typeof(NotificationOrderListExt))]
public async Task<ActionResult<NotificationOrderListExt>> GetBySendersRef([FromQuery, BindRequired] string sendersReference)
{
string? expectedCreator = User.GetOrg();
string? expectedCreator = HttpContext.Items["Org"] as string;
acn-sbuad marked this conversation as resolved.
Show resolved Hide resolved
if (expectedCreator == null)
{
return Forbid();
Expand Down Expand Up @@ -97,7 +97,7 @@ public async Task<ActionResult<NotificationOrderListExt>> GetBySendersRef([FromQ
[SwaggerResponse(404, "No order with the provided id was found")]
public async Task<ActionResult<NotificationOrderWithStatusExt>> GetWithStatusById([FromRoute] Guid id)
{
string? expectedCreator = User.GetOrg();
string? expectedCreator = HttpContext.Items["Org"] as string;
if (expectedCreator == null)
{
return Forbid();
Expand Down
28 changes: 0 additions & 28 deletions src/Altinn.Notifications/Extensions/ClaimsPrincipalExtensions.cs

This file was deleted.

94 changes: 94 additions & 0 deletions src/Altinn.Notifications/Middleware/OrgExtractorMiddleware.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,94 @@
using System.IdentityModel.Tokens.Jwt;
using System.Security.Claims;

using AltinnCore.Authentication.Constants;

namespace Altinn.Notifications.Middleware;

/// <summary>
/// Middleware for extracting org information in an HTTP request
/// from either the issuer of PlatformAccessToken header or as
/// an org claim in the bearer token.
/// </summary>
public class OrgExtractorMiddleware
{
private readonly RequestDelegate _next;

/// <summary>
/// Initializes a new instance of the <see cref="OrgExtractorMiddleware"/> class.
/// </summary>
public OrgExtractorMiddleware(RequestDelegate next)
{
_next = next;
}

/// <summary>
/// Retrieve org claim and save in httpContext as Creator item.
/// </summary>
public async Task InvokeAsync(HttpContext context)
{
if (ShouldApplyMiddleware(context.Request.Path))
{
string? org = GetOrgFromHttpContext(context);

if (org != null)
{
context.Items["Org"] = org;
}
}

await _next(context);
}

private static string? GetOrgFromHttpContext(HttpContext context)
{
string? accessToken = context.Request.Headers["PlatformAccessToken"];
if (!string.IsNullOrEmpty(accessToken))
{
return GetIssuerOfAccessToken(accessToken);
}

return GetOrgFromClaim(context.User);
}

private static string GetIssuerOfAccessToken(string accessToken)
{
JwtSecurityTokenHandler validator = new();
JwtSecurityToken jwt = validator.ReadJwtToken(accessToken);
return jwt.Issuer;
}

private static string? GetOrgFromClaim(ClaimsPrincipal user)
{
if (user.HasClaim(c => c.Type == AltinnCoreClaimTypes.Org))
{
Claim? orgClaim = user.FindFirst(c => c.Type == AltinnCoreClaimTypes.Org);
if (orgClaim != null)
{
return orgClaim.Value;
}
}

return null;
}

private static bool ShouldApplyMiddleware(string path)
{
return !(path.Contains("/trigger") || path.Contains("/health"));
}
}

/// <summary>
/// Static class for middleware registration
/// </summary>
public static class OrgExtractorMiddlewareExtensions
{
/// <summary>
/// Registers the <see cref="OrgExtractorMiddleware"/> in the application
/// </summary>
public static IApplicationBuilder UseOrgExtractor(
this IApplicationBuilder builder)
{
return builder.UseMiddleware<OrgExtractorMiddleware>();
}
}
36 changes: 33 additions & 3 deletions src/Altinn.Notifications/Program.cs
Original file line number Diff line number Diff line change
@@ -1,13 +1,18 @@
#nullable disable

Check warning on line 1 in src/Altinn.Notifications/Program.cs

View workflow job for this annotation

GitHub Actions / Build, test & analyze

Refactor this top-level file to reduce its Cognitive Complexity from 17 to the 15 allowed. (https://rules.sonarsource.com/csharp/RSPEC-3776)

Check warning on line 1 in src/Altinn.Notifications/Program.cs

View workflow job for this annotation

GitHub Actions / Build, test & analyze

Refactor this top-level file to reduce its Cognitive Complexity from 17 to the 15 allowed. (https://rules.sonarsource.com/csharp/RSPEC-3776)
using System.Reflection;
using System.Text.Json;
using System.Text.Json.Serialization;

using Altinn.Common.AccessToken;
using Altinn.Common.AccessToken.Services;
using Altinn.Common.PEP.Authorization;
using Altinn.Notifications.Authorization;
using Altinn.Notifications.Configuration;
using Altinn.Notifications.Core.Extensions;
using Altinn.Notifications.Extensions;
using Altinn.Notifications.Health;
using Altinn.Notifications.Integrations.Extensions;
using Altinn.Notifications.Middleware;
using Altinn.Notifications.Models;
using Altinn.Notifications.Persistence.Extensions;
using Altinn.Notifications.Validators;
Expand All @@ -23,6 +28,7 @@
using Microsoft.ApplicationInsights.Channel;
using Microsoft.ApplicationInsights.Extensibility;
using Microsoft.ApplicationInsights.WindowsServer.TelemetryChannel;
using Microsoft.AspNetCore.Authorization;
using Microsoft.IdentityModel.Tokens;

using Swashbuckle.AspNetCore.Filters;
Expand All @@ -45,11 +51,12 @@
builder.Services.AddSwaggerGen(c =>
{
IncludeXmlComments(c);
c.EnableAnnotations();
c.EnableAnnotations();
c.OperationFilter<AddResponseHeadersFilter>();
});

var app = builder.Build();

app.SetUpPostgreSql(builder.Environment.IsDevelopment(), builder.Configuration);

// Configure the HTTP request pipeline.
Expand All @@ -65,13 +72,15 @@

app.MapHealthChecks("/health");

app.UseOrgExtractor();

app.Run();

void ConfigureSetupLogging()
{
var logFactory = LoggerFactory.Create(builder =>
{
builder

Check warning on line 83 in src/Altinn.Notifications/Program.cs

View workflow job for this annotation

GitHub Actions / Build, test & analyze

Make sure that this logger's configuration is safe. (https://rules.sonarsource.com/csharp/RSPEC-4792)

Check warning on line 83 in src/Altinn.Notifications/Program.cs

View workflow job for this annotation

GitHub Actions / Build, test & analyze

Make sure that this logger's configuration is safe. (https://rules.sonarsource.com/csharp/RSPEC-4792)
.AddFilter("Altinn.Platform.Notifications.Program", LogLevel.Debug)
.AddConsole();
});
Expand Down Expand Up @@ -110,7 +119,7 @@
// If not application insight is available log to console
logging.AddFilter("Microsoft", LogLevel.Warning);
logging.AddFilter("System", LogLevel.Warning);
logging.AddConsole();

Check warning on line 122 in src/Altinn.Notifications/Program.cs

View workflow job for this annotation

GitHub Actions / Build, test & analyze

Make sure that this logger's configuration is safe. (https://rules.sonarsource.com/csharp/RSPEC-4792)
}
}

Expand All @@ -131,7 +140,7 @@
services.AddSingleton(config);
if (!string.IsNullOrEmpty(applicationInsightsConnectionString))
{
services.AddSingleton(typeof(ITelemetryChannel), new ServerTelemetryChannel() { StorageFolder = "/tmp/logtelemetry" });

Check warning on line 143 in src/Altinn.Notifications/Program.cs

View workflow job for this annotation

GitHub Actions / Build, test & analyze

Make sure publicly writable directories are used safely here. (https://rules.sonarsource.com/csharp/RSPEC-5443)

services.AddApplicationInsightsTelemetry(new ApplicationInsightsServiceOptions
{
Expand All @@ -144,7 +153,6 @@
}

GeneralSettings generalSettings = config.GetSection("GeneralSettings").Get<GeneralSettings>();

services.Configure<GeneralSettings>(config.GetSection("GeneralSettings"));
services.AddAuthentication(JwtCookieDefaults.AuthenticationScheme)
.AddJwtCookie(JwtCookieDefaults.AuthenticationScheme, options =>
Expand All @@ -167,17 +175,39 @@
}
});

AddAuthorizationRulesAndHandlers(services, config);

ResourceLinkExtensions.Initialize(generalSettings.BaseUri);
AddInputModelValidators(services);
services.AddCoreServices(config);

services.AddKafkaServices(config);
services.AddKafkaHealthChecks(config);

services.AddPostgresRepositories(config);
services.AddPostgresRepositories(config);
services.AddPostgresHealthChecks(config);
}

void AddAuthorizationRulesAndHandlers(IServiceCollection services, IConfiguration config)
{
services.AddAuthorization(options =>
{
options.AddPolicy(AuthorizationConstants.POLICY_CREATE_SCOPE_OR_PLATFORM_ACCESS, policy =>
{
policy.Requirements.Add(new CreateScopeOrAccessTokenRequirement(AuthorizationConstants.SCOPE_NOTIFICATIONS_CREATE));
});
});

services.AddTransient<IAuthorizationHandler, ScopeAccessHandler>();

// services required for access token handler
services.AddMemoryCache();
services.AddSingleton<IHttpContextAccessor, HttpContextAccessor>();
services.AddSingleton<IPublicSigningKeyProvider, PublicSigningKeyProvider>();
services.Configure<Altinn.Common.AccessToken.Configuration.KeyVaultSettings>(config.GetSection("kvSetting"));
services.AddSingleton<IAuthorizationHandler, AccessTokenHandler>();
}

async Task SetConfigurationProviders(ConfigurationManager config)
{
string basePath = Directory.GetParent(Directory.GetCurrentDirectory()).FullName;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -29,10 +29,6 @@
</AdditionalFiles>
</ItemGroup>

<ItemGroup>
<None Remove="appsettings.IntegrationTest.json" />
</ItemGroup>

<ItemGroup>
<Content Include="appsettings.IntegrationTest.json">
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
Expand Down
Loading
Loading