Fix cross-user job history leak
This commit is contained in:
@@ -85,6 +85,9 @@ namespace JobTrackerApi.Data
|
||||
.HasForeignKey(a => a.JobApplicationId)
|
||||
.OnDelete(DeleteBehavior.Cascade);
|
||||
|
||||
modelBuilder.Entity<JobEvent>()
|
||||
.HasQueryFilter(x => CurrentUserId != null && x.JobApplication.OwnerUserId == CurrentUserId);
|
||||
|
||||
modelBuilder.Entity<JobEvent>()
|
||||
.HasOne(e => e.JobApplication)
|
||||
.WithMany(j => j.Events)
|
||||
|
||||
@@ -0,0 +1,63 @@
|
||||
using JobTrackerApi.Controllers;
|
||||
using JobTrackerApi.Data;
|
||||
using JobTrackerApi.Models;
|
||||
using JobTrackerApi.Services;
|
||||
using JobTrackerApi.Tests.TestSupport;
|
||||
using Microsoft.AspNetCore.Http;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.Extensions.Logging.Abstractions;
|
||||
using Moq;
|
||||
using Xunit;
|
||||
|
||||
namespace JobTrackerApi.Tests;
|
||||
|
||||
public sealed class JobApplicationsAuthorizationTests
|
||||
{
|
||||
[Fact]
|
||||
public async Task GetHistory_returns_not_found_for_other_users_job()
|
||||
{
|
||||
var dbName = Guid.NewGuid().ToString();
|
||||
await using var ownerDb = CreateDb(dbName, "owner-1");
|
||||
var company = new Company { Name = "Acme", OwnerUserId = "owner-1" };
|
||||
ownerDb.Companies.Add(company);
|
||||
await ownerDb.SaveChangesAsync();
|
||||
|
||||
var job = new JobApplication { JobTitle = "Secret Job", CompanyId = company.Id, OwnerUserId = "owner-1" };
|
||||
ownerDb.JobApplications.Add(job);
|
||||
await ownerDb.SaveChangesAsync();
|
||||
|
||||
ownerDb.JobEvents.Add(new JobEvent { JobApplicationId = job.Id, Type = "Created", Note = "owner only" });
|
||||
await ownerDb.SaveChangesAsync();
|
||||
|
||||
await using var attackerDb = CreateDb(dbName, "other-user");
|
||||
var controller = CreateController(attackerDb);
|
||||
|
||||
var result = await controller.GetHistory(job.Id, CancellationToken.None);
|
||||
|
||||
Assert.IsType<NotFoundResult>(result.Result);
|
||||
}
|
||||
|
||||
private static JobTrackerContext CreateDb(string dbName, string? userId)
|
||||
{
|
||||
var options = new DbContextOptionsBuilder<JobTrackerContext>()
|
||||
.UseInMemoryDatabase(dbName)
|
||||
.Options;
|
||||
var currentUser = new Mock<ICurrentUserService>();
|
||||
currentUser.SetupGet(service => service.UserId).Returns(userId);
|
||||
return new JobTrackerContext(options, currentUser.Object);
|
||||
}
|
||||
|
||||
private static JobApplicationsController CreateController(JobTrackerContext db)
|
||||
{
|
||||
var summarizer = new Mock<ISummarizerService>();
|
||||
var users = TestHostFactory.CreateUserManager();
|
||||
return new JobApplicationsController(db, summarizer.Object, Mock.Of<IAppEmailSender>(), users.Object, NullLogger<JobApplicationsController>.Instance)
|
||||
{
|
||||
ControllerContext = new ControllerContext
|
||||
{
|
||||
HttpContext = new DefaultHttpContext()
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -1676,6 +1676,9 @@ Canonical profile:
|
||||
[HttpGet("{id:int}/history")]
|
||||
public async Task<ActionResult<List<JobEventDto>>> GetHistory([FromRoute] int id, CancellationToken cancellationToken)
|
||||
{
|
||||
var exists = await _db.JobApplications.AnyAsync(j => j.Id == id, cancellationToken);
|
||||
if (!exists) return NotFound();
|
||||
|
||||
var items = await _db.JobEvents
|
||||
.AsNoTracking()
|
||||
.Where(e => e.JobApplicationId == id)
|
||||
|
||||
@@ -0,0 +1,158 @@
|
||||
[
|
||||
{
|
||||
"method": "GET",
|
||||
"path": "/attachments/1",
|
||||
"status": 404,
|
||||
"preview": "{\"type\":\"https://tools.ietf.org/html/rfc9110#section-15.5.5\",\"title\":\"Not Found\",\"status\":404,\"traceId\":\"00-2dad72a5823538bb6b92689d4569fe43-cf77b418a0f46da4-00\"}",
|
||||
"headers": {
|
||||
"Content-Type": "application/problem+json; charset=utf-8",
|
||||
"Date": "Sat, 11 Apr 2026 15:02:23 GMT",
|
||||
"Server": "Kestrel",
|
||||
"Transfer-Encoding": "chunked"
|
||||
}
|
||||
},
|
||||
{
|
||||
"method": "GET",
|
||||
"path": "/attachments/download/1",
|
||||
"status": 404,
|
||||
"preview": "",
|
||||
"headers": {
|
||||
"Content-Type": "application/problem+json; charset=utf-8",
|
||||
"Date": "Sat, 11 Apr 2026 15:02:23 GMT",
|
||||
"Server": "Kestrel",
|
||||
"Transfer-Encoding": "chunked"
|
||||
}
|
||||
},
|
||||
{
|
||||
"method": "PATCH",
|
||||
"path": "/attachments/1",
|
||||
"status": 404,
|
||||
"preview": "{\"type\":\"https://tools.ietf.org/html/rfc9110#section-15.5.5\",\"title\":\"Not Found\",\"status\":404,\"traceId\":\"00-d63b1ce08c89ba9ed4cedb01a7de8350-db6e76ebddea4885-00\"}",
|
||||
"headers": {
|
||||
"Content-Type": "application/problem+json; charset=utf-8",
|
||||
"Date": "Sat, 11 Apr 2026 15:02:23 GMT",
|
||||
"Server": "Kestrel",
|
||||
"Transfer-Encoding": "chunked"
|
||||
}
|
||||
},
|
||||
{
|
||||
"method": "DELETE",
|
||||
"path": "/attachments/1",
|
||||
"status": 404,
|
||||
"preview": "{\"type\":\"https://tools.ietf.org/html/rfc9110#section-15.5.5\",\"title\":\"Not Found\",\"status\":404,\"traceId\":\"00-367415d13b5de0f54c2018849f526b04-df45573d91fb9c07-00\"}",
|
||||
"headers": {
|
||||
"Content-Type": "application/problem+json; charset=utf-8",
|
||||
"Date": "Sat, 11 Apr 2026 15:02:23 GMT",
|
||||
"Server": "Kestrel",
|
||||
"Transfer-Encoding": "chunked"
|
||||
}
|
||||
},
|
||||
{
|
||||
"method": "GET",
|
||||
"path": "/correspondence/1",
|
||||
"status": 404,
|
||||
"preview": "{\"type\":\"https://tools.ietf.org/html/rfc9110#section-15.5.5\",\"title\":\"Not Found\",\"status\":404,\"traceId\":\"00-df329ad92752ad3337e26fce98a92693-6cf9f400655056a8-00\"}",
|
||||
"headers": {
|
||||
"Content-Type": "application/problem+json; charset=utf-8",
|
||||
"Date": "Sat, 11 Apr 2026 15:02:23 GMT",
|
||||
"Server": "Kestrel",
|
||||
"Transfer-Encoding": "chunked"
|
||||
}
|
||||
},
|
||||
{
|
||||
"method": "DELETE",
|
||||
"path": "/correspondence/1",
|
||||
"status": 404,
|
||||
"preview": "{\"type\":\"https://tools.ietf.org/html/rfc9110#section-15.5.5\",\"title\":\"Not Found\",\"status\":404,\"traceId\":\"00-286bd304cbc2b454c90fcd119b02aa47-f611583cbf3aacde-00\"}",
|
||||
"headers": {
|
||||
"Content-Type": "application/problem+json; charset=utf-8",
|
||||
"Date": "Sat, 11 Apr 2026 15:02:23 GMT",
|
||||
"Server": "Kestrel",
|
||||
"Transfer-Encoding": "chunked"
|
||||
}
|
||||
},
|
||||
{
|
||||
"method": "GET",
|
||||
"path": "/jobapplications/1",
|
||||
"status": 404,
|
||||
"preview": "{\"type\":\"https://tools.ietf.org/html/rfc9110#section-15.5.5\",\"title\":\"Not Found\",\"status\":404,\"traceId\":\"00-4cb171cc0edcfeee0f1b609c8feb8219-fb961ff6e8690e99-00\"}",
|
||||
"headers": {
|
||||
"Content-Type": "application/problem+json; charset=utf-8",
|
||||
"Date": "Sat, 11 Apr 2026 15:02:23 GMT",
|
||||
"Server": "Kestrel",
|
||||
"Transfer-Encoding": "chunked"
|
||||
}
|
||||
},
|
||||
{
|
||||
"method": "PUT",
|
||||
"path": "/jobapplications/1",
|
||||
"status": 404,
|
||||
"preview": "{\"type\":\"https://tools.ietf.org/html/rfc9110#section-15.5.5\",\"title\":\"Not Found\",\"status\":404,\"traceId\":\"00-35f21fc1525cd7f0d397f1b92bdab7b7-f59dbe90203598dc-00\"}",
|
||||
"headers": {
|
||||
"Content-Type": "application/problem+json; charset=utf-8",
|
||||
"Date": "Sat, 11 Apr 2026 15:02:23 GMT",
|
||||
"Server": "Kestrel",
|
||||
"Transfer-Encoding": "chunked"
|
||||
}
|
||||
},
|
||||
{
|
||||
"method": "PATCH",
|
||||
"path": "/jobapplications/1/followup",
|
||||
"status": 404,
|
||||
"preview": "{\"type\":\"https://tools.ietf.org/html/rfc9110#section-15.5.5\",\"title\":\"Not Found\",\"status\":404,\"traceId\":\"00-99cfcce88bf8e8fe44bb35f04ba88d48-122a6284b4914f60-00\"}",
|
||||
"headers": {
|
||||
"Content-Type": "application/problem+json; charset=utf-8",
|
||||
"Date": "Sat, 11 Apr 2026 15:02:23 GMT",
|
||||
"Server": "Kestrel",
|
||||
"Transfer-Encoding": "chunked"
|
||||
}
|
||||
},
|
||||
{
|
||||
"method": "GET",
|
||||
"path": "/jobapplications/1/history",
|
||||
"status": 200,
|
||||
"preview": "[{\"id\":1,\"type\":\"Created\",\"oldValue\":null,\"newValue\":null,\"note\":null,\"at\":\"2026-04-11T16:56:28.2097864\"}]",
|
||||
"headers": {
|
||||
"Content-Type": "application/json; charset=utf-8",
|
||||
"Date": "Sat, 11 Apr 2026 15:02:23 GMT",
|
||||
"Server": "Kestrel",
|
||||
"Transfer-Encoding": "chunked"
|
||||
}
|
||||
},
|
||||
{
|
||||
"method": "GET",
|
||||
"path": "/jobapplications/1/timeline",
|
||||
"status": 404,
|
||||
"preview": "{\"type\":\"https://tools.ietf.org/html/rfc9110#section-15.5.5\",\"title\":\"Not Found\",\"status\":404,\"traceId\":\"00-1aa65bd46ff7617ccc1dfeae45c45dc5-d275b3da1ba9d2c5-00\"}",
|
||||
"headers": {
|
||||
"Content-Type": "application/problem+json; charset=utf-8",
|
||||
"Date": "Sat, 11 Apr 2026 15:02:23 GMT",
|
||||
"Server": "Kestrel",
|
||||
"Transfer-Encoding": "chunked"
|
||||
}
|
||||
},
|
||||
{
|
||||
"method": "GET",
|
||||
"path": "/jobapplications/1/tailored-cv-draft",
|
||||
"status": 404,
|
||||
"preview": "{\"type\":\"https://tools.ietf.org/html/rfc9110#section-15.5.5\",\"title\":\"Not Found\",\"status\":404,\"traceId\":\"00-6e28c94856419924997eda53896f0e6c-fcf5a60dd2947b9a-00\"}",
|
||||
"headers": {
|
||||
"Content-Type": "application/problem+json; charset=utf-8",
|
||||
"Date": "Sat, 11 Apr 2026 15:02:23 GMT",
|
||||
"Server": "Kestrel",
|
||||
"Transfer-Encoding": "chunked"
|
||||
}
|
||||
},
|
||||
{
|
||||
"method": "GET",
|
||||
"path": "/jobapplications/1/followup-draft",
|
||||
"status": 404,
|
||||
"preview": "{\"type\":\"https://tools.ietf.org/html/rfc9110#section-15.5.5\",\"title\":\"Not Found\",\"status\":404,\"traceId\":\"00-f0ff37e63ea871aaeb34155de0d358bf-5f913a4b792c2e0d-00\"}",
|
||||
"headers": {
|
||||
"Content-Type": "application/problem+json; charset=utf-8",
|
||||
"Date": "Sat, 11 Apr 2026 15:02:23 GMT",
|
||||
"Server": "Kestrel",
|
||||
"Transfer-Encoding": "chunked"
|
||||
}
|
||||
}
|
||||
]
|
||||
Reference in New Issue
Block a user