Skip to content

Commit 80cbba5

Browse files
committed
Merge branch 'main' into VFD-289-aurora-rds-postgresql-serverless-v-2-pystytys
2 parents 7174241 + 944413e commit 80cbba5

27 files changed

Lines changed: 482 additions & 230 deletions

VirtualFinland.UserAPI/src/VirtualFinland.UsersAPI/Activities/Productizer/ProductizerController.cs

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ namespace VirtualFinland.UserAPI.Activities.Productizer;
1313

1414
[ApiController]
1515
[Authorize]
16+
[Authorize(Policy = "RequestFromDataspace")]
1617
[ProducesResponseType(StatusCodes.Status401Unauthorized)]
1718
[Produces("application/json")]
1819
public class ProductizerController : ControllerBase
@@ -83,7 +84,7 @@ public async Task<IActionResult> GetPersonBasicInformation()
8384

8485
var result = await _mediator.Send(new GetPersonBasicInformation.Query(userId));
8586

86-
if (!ProductizerProfileValidator.IsPersonBasicInformationCreated(result)) return NotFound();
87+
if (!ProductizerProfileValidator.IsPersonBasicInformationCreated(result)) throw new NotFoundException("Person not found");
8788

8889
return Ok(result);
8990
}
@@ -121,7 +122,7 @@ public async Task<IActionResult> GetPersonJobApplicantInformation()
121122

122123
var result = await _mediator.Send(new GetJobApplicantProfile.Query(userId));
123124

124-
if (!ProductizerProfileValidator.IsJobApplicantProfileCreated(result)) return NotFound();
125+
if (!ProductizerProfileValidator.IsJobApplicantProfileCreated(result)) throw new NotFoundException("Job applicant profile not found");
125126

126127
return Ok(result);
127128
}

VirtualFinland.UserAPI/src/VirtualFinland.UsersAPI/Activities/User/UserController.cs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ namespace VirtualFinland.UserAPI.Activities.User;
1111

1212
[ApiController]
1313
[Authorize]
14+
[Authorize(Policy = "RequestFromAccessFinland")]
1415
[ProducesResponseType(StatusCodes.Status401Unauthorized)]
1516
[Produces("application/json")]
1617
public class UserController : ApiControllerBase

VirtualFinland.UserAPI/src/VirtualFinland.UsersAPI/Data/UsersDBContext.cs

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -8,14 +8,17 @@ namespace VirtualFinland.UserAPI.Data;
88
public class UsersDbContext : DbContext
99
{
1010
private readonly bool _isTesting;
11+
private readonly IAuditInterceptor _auditInterceptor;
1112

12-
public UsersDbContext(DbContextOptions options) : base(options)
13+
public UsersDbContext(DbContextOptions options, IAuditInterceptor auditInterceptor) : base(options)
1314
{
15+
_auditInterceptor = auditInterceptor;
1416
}
1517

16-
public UsersDbContext(DbContextOptions options, bool isTesting) : base(options)
18+
public UsersDbContext(DbContextOptions options, IAuditInterceptor auditInterceptor, bool isTesting) : base(options)
1719
{
1820
_isTesting = isTesting;
21+
_auditInterceptor = auditInterceptor;
1922
}
2023

2124
public DbSet<ExternalIdentity> ExternalIdentities => Set<ExternalIdentity>();
@@ -32,7 +35,7 @@ public UsersDbContext(DbContextOptions options, bool isTesting) : base(options)
3235

3336
protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
3437
{
35-
optionsBuilder.AddInterceptors(new AuditInterceptor());
38+
optionsBuilder.AddInterceptors(_auditInterceptor);
3639
}
3740

3841
protected override void OnModelCreating(ModelBuilder modelBuilder)
Lines changed: 57 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,27 @@
11
using Microsoft.EntityFrameworkCore;
2+
using Microsoft.EntityFrameworkCore.ChangeTracking;
23
using Microsoft.EntityFrameworkCore.Diagnostics;
34
using VirtualFinland.UserAPI.Models.UsersDatabase;
45

56
namespace VirtualFinland.UserAPI.Helpers;
67

8+
public interface IAuditInterceptor : IInterceptor
9+
{
10+
}
11+
12+
713
/// <summary>
814
/// Interceptor used to automatically set created and modified property values on classes that inherit from
915
/// <see cref="Auditable" /> abstract class
1016
/// </summary>
11-
public class AuditInterceptor : SaveChangesInterceptor
17+
public class AuditInterceptor : SaveChangesInterceptor, IAuditInterceptor
1218
{
19+
private readonly ILogger<IAuditInterceptor> _logger;
20+
public AuditInterceptor(ILogger<IAuditInterceptor> logger) : base()
21+
{
22+
_logger = logger;
23+
}
24+
1325
public override ValueTask<InterceptionResult<int>> SavingChangesAsync(
1426
DbContextEventData eventData,
1527
InterceptionResult<int> result,
@@ -18,24 +30,53 @@ public override ValueTask<InterceptionResult<int>> SavingChangesAsync(
1830
if (eventData.Context is null)
1931
throw new ArgumentNullException(nameof(eventData), $"{nameof(eventData)} is null");
2032

21-
var insertedEntries = eventData.Context.ChangeTracker
22-
.Entries()
23-
.Where(x => x.State == EntityState.Added)
24-
.Select(x => x.Entity);
33+
foreach (var entry in eventData.Context.ChangeTracker.Entries())
34+
{
35+
switch (entry.State)
36+
{
37+
case EntityState.Added:
38+
case EntityState.Modified:
39+
case EntityState.Deleted:
40+
if (entry.Entity is Auditable)
41+
{
42+
_logger.LogInformation("@{AuditLog}", _CreateAuditLog(entry));
43+
}
44+
break;
45+
}
46+
}
47+
48+
return base.SavingChangesAsync(eventData, result, cancellationToken);
49+
}
2550

26-
foreach (var insertedEntry in insertedEntries)
27-
if (insertedEntry is Auditable auditableEntity)
28-
auditableEntity.Created = DateTime.UtcNow;
51+
private AuditLog _CreateAuditLog(EntityEntry entry)
52+
{
53+
var (primaryKeys, nonPrimaryKeys) = _GetLogMessageColumns(entry);
54+
return new AuditLog
55+
{
56+
TableName = entry.Metadata.DisplayName(),
57+
Action = entry.State.ToString(),
58+
KeyValues = primaryKeys.Any() ? $"[{string.Join(", ", primaryKeys)}]" : "[]",
59+
ChangedColumns = nonPrimaryKeys.Any() ? $"[{string.Join(", ", nonPrimaryKeys)}]" : "[]",
60+
EventDate = DateTime.UtcNow
61+
};
62+
}
2963

30-
var modifiedEntries = eventData.Context.ChangeTracker
31-
.Entries()
32-
.Where(x => x.State == EntityState.Modified)
33-
.Select(x => x.Entity);
64+
private Tuple<List<string>, List<string>> _GetLogMessageColumns(EntityEntry entry)
65+
{
66+
var primaryKeys = entry.Properties.Where(property => property.Metadata.IsPrimaryKey())
67+
.Select(property => $"{property.Metadata.Name} = {property.CurrentValue}");
68+
var nonPrimaryKeys = entry.Properties.Where(property => !property.Metadata.IsPrimaryKey() && (entry.State != EntityState.Modified || property.IsModified))
69+
.Select(property => property.Metadata.Name);
3470

35-
foreach (var modifiedEntry in modifiedEntries)
36-
if (modifiedEntry is Auditable auditableEntity)
37-
auditableEntity.Modified = DateTime.UtcNow;
71+
return new Tuple<List<string>, List<string>>(primaryKeys.ToList(), nonPrimaryKeys.ToList());
72+
}
3873

39-
return base.SavingChangesAsync(eventData, result, cancellationToken);
74+
public record AuditLog
75+
{
76+
public string TableName { get; init; } = default!;
77+
public string Action { get; init; } = default!;
78+
public string KeyValues { get; init; } = default!;
79+
public string ChangedColumns { get; init; } = default!;
80+
public DateTime EventDate { get; init; } = default!;
4081
}
4182
}

VirtualFinland.UserAPI/src/VirtualFinland.UsersAPI/Helpers/Constants.cs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,8 @@ public static class Web
1111
public static class Security
1212
{
1313
public static string ResolvePolicyFromTokenIssuer => "ResolvePolicyFromTokenIssuer";
14+
public static string RequestFromAccessFinland => "RequestFromAccessFinland";
15+
public static string RequestFromDataspace => "RequestFromDataspace";
1416
}
1517

1618
public static class Headers

VirtualFinland.UserAPI/src/VirtualFinland.UsersAPI/Middleware/ErrorHandlerMiddleware.cs

Lines changed: 29 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -10,11 +10,12 @@ public class ErrorHandlerMiddleware
1010
private readonly ILogger<ErrorHandlerMiddleware> _logger;
1111

1212
/// <summary>
13-
/// RFC7807 Problem Details
13+
/// Dataspace error response details
1414
/// </summary>
15-
private class ErrorResponseDetails : Microsoft.AspNetCore.Mvc.ProblemDetails
15+
private class ErrorResponseDetails
1616
{
17-
17+
public string Type { get; set; } = string.Empty;
18+
public string Message { get; set; } = string.Empty;
1819
}
1920

2021
public ErrorHandlerMiddleware(RequestDelegate next, ILogger<ErrorHandlerMiddleware> logger)
@@ -30,56 +31,54 @@ public async Task Invoke(HttpContext context)
3031
}
3132
catch (Exception error)
3233
{
33-
_logger.LogError(error, "Request processing failure!");
34+
if (error is not NotFoundException && error is not NotAuthorizedException)
35+
{
36+
_logger.LogError(error, "Request processing failure!");
37+
}
38+
3439
var response = context.Response;
3540
response.ContentType = "application/json";
36-
Dictionary<string, List<string>> validationErrorDetails = new Dictionary<string, List<string>>();
3741

38-
switch(error)
42+
ErrorResponseDetails errorResponseDetails = new()
43+
{
44+
Type = "",
45+
Message = ""
46+
};
47+
48+
switch (error)
3949
{
4050
case NotAuthorizedException:
4151
// custom application error
4252
response.StatusCode = (int)HttpStatusCode.Unauthorized;
53+
errorResponseDetails.Type = "Unauthorized";
54+
errorResponseDetails.Message = error.Message ?? "Not authorized";
4355
break;
4456
case NotFoundException:
4557
// not found error
4658
response.StatusCode = (int)HttpStatusCode.NotFound;
59+
errorResponseDetails.Type = "NotFound";
60+
errorResponseDetails.Message = error.Message ?? "Not found";
4761
break;
48-
case BadRequestException e:
62+
case BadRequestException:
4963
// bad request error
5064
response.StatusCode = (int)HttpStatusCode.BadRequest;
51-
52-
e.ValidationErrors?.ForEach( o => validationErrorDetails.Add(o.Field, new List<string>() { o.Message }));
65+
errorResponseDetails.Type = "BadRequest";
66+
errorResponseDetails.Message = error.Message ?? "Bad request";
5367
break;
5468
default:
5569
// unhandled error
5670
response.StatusCode = (int)HttpStatusCode.InternalServerError;
71+
errorResponseDetails.Type = "InternalServerError";
72+
errorResponseDetails.Message = error.Message ?? "Internal Server Error";
5773
break;
5874
}
5975

60-
ErrorResponseDetails errorResponseDetails = validationErrorDetails?.Count == 0 ? new ErrorResponseDetails()
61-
{
62-
Type = "https://tools.ietf.org/html/rfc7231",
63-
Title = error.Message,
64-
Detail = error.Message,
65-
Status = response.StatusCode,
66-
Instance = response.HttpContext.Request.Path
67-
} : new ErrorResponseDetails()
68-
{
69-
Type = "https://tools.ietf.org/html/rfc7231",
70-
Title = error.Message,
71-
Detail = error.Message,
72-
Status = response.StatusCode,
73-
Instance = response.HttpContext.Request.Path,
74-
Extensions = { new KeyValuePair<string, object?>( "errors", validationErrorDetails) }
75-
};
76-
7776
var result = JsonSerializer.Serialize(errorResponseDetails,
7877
new JsonSerializerOptions
79-
{
80-
PropertyNamingPolicy = JsonNamingPolicy.CamelCase,
81-
WriteIndented = true
82-
});
78+
{
79+
PropertyNamingPolicy = JsonNamingPolicy.CamelCase,
80+
WriteIndented = true
81+
});
8382
await response.WriteAsync(result);
8483
}
8584
}

VirtualFinland.UserAPI/src/VirtualFinland.UsersAPI/Program.cs

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@
1414
using VirtualFinland.UserAPI.Middleware;
1515
using VirtualFinland.UserAPI.Helpers.Extensions;
1616
using VirtualFinland.UserAPI.Security.Extensions;
17+
using VirtualFinland.UserAPI.Helpers;
1718

1819
Log.Logger = new LoggerConfiguration()
1920
.WriteTo.Console()
@@ -87,6 +88,7 @@
8788
: null;
8889
var dbConnectionString = databaseSecret ?? builder.Configuration.GetConnectionString("DefaultConnection");
8990

91+
builder.Services.AddSingleton<IAuditInterceptor, AuditInterceptor>();
9092
builder.Services.AddDbContext<UsersDbContext>(options =>
9193
{
9294
options.UseNpgsql(dbConnectionString,
@@ -141,8 +143,9 @@
141143
.AllowAnyHeader());
142144
}
143145

144-
app.UseMiddleware<ErrorHandlerMiddleware>();
146+
145147
app.UseSerilogRequestLogging();
148+
app.UseMiddleware<ErrorHandlerMiddleware>();
146149
app.UseHttpsRedirection();
147150
app.UseAuthentication();
148151
app.UseAuthorization();
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
2+
namespace VirtualFinland.UserAPI.Security.AccessRequirements;
3+
4+
public sealed class RequestAccessConfig
5+
{
6+
public string HeaderName { get; init; } = default!;
7+
public List<string> AccessKeys { get; init; } = default!;
8+
public bool IsEnabled { get; init; } = default!;
9+
}
Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
using Microsoft.AspNetCore.Authorization;
2+
using Microsoft.Extensions.Options;
3+
4+
namespace VirtualFinland.UserAPI.Security.AccessRequirements;
5+
6+
public class RequestAccessRequirement : AuthorizationHandler<RequestAccessRequirement>, IAuthorizationRequirement
7+
{
8+
private readonly RequestAccessConfig _config;
9+
public RequestAccessRequirement(RequestAccessConfig config)
10+
{
11+
_config = config;
12+
}
13+
14+
protected override Task HandleRequirementAsync(AuthorizationHandlerContext context, RequestAccessRequirement requirement)
15+
{
16+
if (!_config.IsEnabled)
17+
context.Succeed(requirement);
18+
19+
if (context.Resource is DefaultHttpContext httpContext &&
20+
httpContext.Request.Headers.TryGetValue(_config.HeaderName, out var apiKeyHeader))
21+
{
22+
string apiKey = apiKeyHeader.ToString();
23+
if (IsValidApiKey(apiKey))
24+
{
25+
context.Succeed(requirement);
26+
}
27+
}
28+
return Task.CompletedTask;
29+
}
30+
31+
private bool IsValidApiKey(string apiKey)
32+
{
33+
foreach (var accessKey in _config.AccessKeys)
34+
{
35+
if (accessKey == "") continue;
36+
if (apiKey == accessKey)
37+
return true;
38+
}
39+
return false;
40+
}
41+
}

VirtualFinland.UserAPI/src/VirtualFinland.UsersAPI/Security/ApplicationSecurity.cs

Lines changed: 2 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -28,13 +28,10 @@ public JwtTokenResult ParseJwtToken(string token)
2828

2929
// Resolve the security feature by token issuer (must be enabled) // @TODO: ensure the security feature is loaded before this
3030
var tokenIssuer = parsedToken.Issuer;
31-
var securityFeature = _features.Find(o => o.Issuer == tokenIssuer);
32-
if (securityFeature == null) throw new NotAuthorizedException("The given token issuer is not valid");
31+
var securityFeature = _features.Find(o => o.Issuer == tokenIssuer) ?? throw new NotAuthorizedException("The given token issuer is not valid");
3332

3433
// Resolve user id
35-
var userId = securityFeature.ResolveTokenUserId(parsedToken);
36-
if (userId == null) throw new NotAuthorizedException("The given token claim is not valid");
37-
34+
var userId = securityFeature.ResolveTokenUserId(parsedToken) ?? throw new NotAuthorizedException("The given token claim is not valid");
3835
return new JwtTokenResult { UserId = userId, Issuer = securityFeature.Issuer };
3936
}
4037
}

0 commit comments

Comments
 (0)