Skip to content

Commit

Permalink
Merge pull request #676 from bcgov/BCPSDEMS-1437-sync-cases
Browse files Browse the repository at this point in the history
security updates and handle case syncing tasks
  • Loading branch information
leewrigh authored Oct 18, 2024
2 parents d0b55c3 + c5d168a commit 3e00e24
Show file tree
Hide file tree
Showing 20 changed files with 366 additions and 43 deletions.
2 changes: 1 addition & 1 deletion backend/CommonModels/CommonModels.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@
</PropertyGroup>

<ItemGroup>
<PackageReference Include="NodaTime" Version="3.1.12" />
<PackageReference Include="NodaTime" Version="3.2.0" />
<PackageReference Include="NodaTime.Serialization.SystemTextJson" Version="1.2.0" />
<PackageReference Include="MediatR" Version="12.4.1" />
<PackageReference Include="Newtonsoft.Json" Version="13.0.3" />
Expand Down
8 changes: 8 additions & 0 deletions backend/CommonModels/Models/EDT/CaseModel.cs
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,14 @@ public class CaseSummaryModel

}


public class UserCaseSearchResponseModel
{
public int Id { get; set; }
public string Status { get; set; } = string.Empty;
public string? Key { get; set; } = string.Empty;
}

public class CaseModel
{

Expand Down
14 changes: 7 additions & 7 deletions backend/common/Common.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -20,9 +20,9 @@
<PackageReference Include="AutoMapper" Version="13.0.1" />
<PackageReference Include="Bogus" Version="35.6.1" />
<PackageReference Include="Chr.Avro.Confluent" Version="10.4.0" />
<PackageReference Include="Confluent.Kafka" Version="2.5.3" />
<PackageReference Include="Confluent.SchemaRegistry" Version="2.5.3" />
<PackageReference Include="Confluent.SchemaRegistry.Serdes.Avro" Version="2.5.3" />
<PackageReference Include="Confluent.Kafka" Version="2.6.0" />
<PackageReference Include="Confluent.SchemaRegistry" Version="2.6.0" />
<PackageReference Include="Confluent.SchemaRegistry.Serdes.Avro" Version="2.6.0" />
<PackageReference Include="DomainResult.Common" Version="3.2.0" />
<PackageReference Include="DomainResult" Version="3.2.0" />
<PackageReference Include="FluentValidation.AspNetCore" Version="11.3.0" />
Expand All @@ -36,12 +36,12 @@
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>
<PackageReference Include="Microsoft.EntityFrameworkCore.Relational" Version="8.0.10" />
<PackageReference Include="Npgsql.EntityFrameworkCore.PostgreSQL" Version="8.0.8" />
<PackageReference Include="Npgsql.EntityFrameworkCore.PostgreSQL.NodaTime" Version="8.0.8" />
<PackageReference Include="Npgsql.EntityFrameworkCore.PostgreSQL" Version="8.0.10" />
<PackageReference Include="Npgsql.EntityFrameworkCore.PostgreSQL.NodaTime" Version="8.0.10" />
<PackageReference Include="Microsoft.Extensions.DependencyInjection" Version="8.0.1" />
<PackageReference Include="Microsoft.Extensions.Diagnostics.HealthChecks" Version="8.0.10" />
<PackageReference Include="Microsoft.AspNetCore.Authentication.JwtBearer" Version="8.0.10" />
<PackageReference Include="Microsoft.AspNetCore.Identity" Version="2.2.0" />
<PackageReference Include="Microsoft.AspNetCore.Identity" Version="2.1.39" />
<PackageReference Include="Microsoft.VisualStudio.Azure.Containers.Tools.Targets" Version="1.21.0" />
<PackageReference Include="Newtonsoft.Json" Version="13.0.3" />
<PackageReference Include="NodaTime.Serialization.JsonNet" Version="3.1.0" />
Expand All @@ -57,7 +57,7 @@
<PackageReference Include="OpenTelemetry.Instrumentation.SqlClient" Version="1.0.0-rc9.11" />
<PackageReference Include="OpenTelemetry.Exporter.OpenTelemetryProtocol" Version="1.9.0" />
<PackageReference Include="OpenTelemetry.Exporter.OpenTelemetryProtocol.Logs" Version="1.0.0-rc8" />
<PackageReference Include="NodaTime" Version="3.1.12" />
<PackageReference Include="NodaTime" Version="3.2.0" />
<PackageReference Include="NodaTime.Serialization.SystemTextJson" Version="1.2.0" />
<PackageReference Include="Polly" Version="8.4.2" />
<PackageReference Include="Serilog" Version="4.0.2" />
Expand Down
4 changes: 2 additions & 2 deletions backend/service.edt/Features/Person/PersonController.cs
Original file line number Diff line number Diff line change
Expand Up @@ -34,9 +34,9 @@ public async Task<ActionResult<EdtPersonDto>> GetUser([FromRoute] PersonQuery qu
[HttpGet("key/{key}")]
[ProducesResponseType(StatusCodes.Status200OK)]
[ProducesResponseType(StatusCodes.Status404NotFound)]
public async Task<ActionResult<EdtPersonDto>> GetUserByKey([FromRoute] PersonLookupModel lookupModel)
public async Task<ActionResult<EdtPersonDto>> GetUserByKey([FromRoute] string key)
{
var search = new PersonSearchQuery(lookupModel);
var search = new PersonByKeyQuery(key);

var c = await this.mediator.Send(search);
if (c == null)
Expand Down
20 changes: 20 additions & 0 deletions backend/service.edt/Features/Users/UserCasesQuery.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
namespace edt.service.Features.Users;

using System.Threading;
using System.Threading.Tasks;
using Common.Models.EDT;
using edt.service.HttpClients.Services.EdtCore;
using MediatR;

public record UserCasesQuery(string UserId) : IRequest<List<UserCaseSearchResponseModel>>;

public class UserCasesQueryHandler(IEdtClient edtClient, ILogger<UserCasesQuery> logger) : IRequestHandler<UserCasesQuery, List<UserCaseSearchResponseModel>>
{
public async Task<List<UserCaseSearchResponseModel>> Handle(UserCasesQuery request, CancellationToken cancellationToken)
{
logger.LogInformation($"Looking up user cases for user {request.UserId}");

return await edtClient.GetUserCases(request.UserId);

}
}
28 changes: 24 additions & 4 deletions backend/service.edt/Features/Users/UserController.cs
Original file line number Diff line number Diff line change
Expand Up @@ -11,23 +11,43 @@ namespace edt.service.Controllers;
[Route("api/[controller]")]
[ApiController]
[Authorize(Policy = Policies.DiamInternalAuthentication)]
public class UserController : ControllerBase
public class UserController(IMediator mediator) : ControllerBase
{
private readonly IMediator _mediator;

[HttpGet("party/{partyId}")]
[HttpGet("party/{userKey}")]
[ProducesResponseType(StatusCodes.Status200OK)]
[ProducesResponseType(StatusCodes.Status404NotFound)]
public async Task<ActionResult<EdtUserDto>> GetUser([FromServices] IRequestHandler<UserQuery, EdtUserDto> handler,
[FromRoute] UserQuery query)
{

var c = await this._mediator.Send(query);
var c = await mediator.Send(query);
if (c == null)
{
return this.NotFound();
}
return this.Ok(c);
}

[HttpGet("party/cases/{UserId}")]
[ProducesResponseType(StatusCodes.Status200OK)]
[ProducesResponseType(StatusCodes.Status404NotFound)]
public async Task<ActionResult<EdtUserDto>> GetUserCases([FromServices] IRequestHandler<UserCasesQuery, List<UserCaseSearchResponseModel>> handler,
[FromRoute] UserCasesQuery query)
{
try
{
var c = await mediator.Send(query);
if (c == null)
{
return this.NotFound();
}
return this.Ok(c);
}
catch (Exception ex)
{
// Return a 500 Internal Server Error response
return this.StatusCode(StatusCodes.Status500InternalServerError);
}
}
}
1 change: 1 addition & 0 deletions backend/service.edt/Features/Users/UserQuery.cs
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ namespace edt.service.Features.Users;
using MediatR;

public record UserQuery(string userKey) : IRequest<EdtUserDto>;

public class UserQueryHandler : IRequestHandler<UserQuery, EdtUserDto>
{
private readonly IEdtClient edtClient;
Expand Down
12 changes: 12 additions & 0 deletions backend/service.edt/HttpClients/Services/EdtCore/EdtClient.cs
Original file line number Diff line number Diff line change
Expand Up @@ -969,6 +969,18 @@ public async Task<List<EdtPersonDto>> SearchForPerson(PersonLookupModel personLo
}
}

public async Task<List<UserCaseSearchResponseModel>> GetUserCases(string userKey)
{
var result = await this.GetAsync<List<UserCaseSearchResponseModel>?>($"api/v1/org-units/1/users/{userKey}/cases");

if (!result.IsSuccess)
{
throw new EdtServiceException(string.Join(",", result.Errors));
}

return result.Value;
}

public class AddUserToOuGroup
{
public string? UserIdOrKey { get; set; }
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,6 @@ public interface IEdtClient
Task<UserModificationEvent> UpdateUserDetails(EdtUserDto userDetails);
Task<bool> LinkPersonToDisclosureFolio(PersonFolioLinkage request);


Task<int> GetOuGroupId(string regionName);

Task<EdtUserDto?> GetUser(string userKey);
Expand Down Expand Up @@ -120,6 +119,5 @@ public interface IEdtClient
/// <param name="personLookup"></param>
/// <returns></returns>
Task<List<EdtPersonDto>> SearchForPerson(PersonLookupModel personLookup);


Task<List<UserCaseSearchResponseModel>> GetUserCases(string userKey);
}
Original file line number Diff line number Diff line change
Expand Up @@ -27,16 +27,19 @@ public static IServiceCollection AddKeycloakAuth(this IServiceCollection service
})
.AddJwtBearer(options =>
{
options.UseSecurityTokenValidators = true;
options.Authority = KeycloakUrls.Authority(RealmConstants.BCPSRealm, config.Keycloak.RealmUrl);
options.RequireHttpsMetadata = false;
options.Audience = "DIAM-INTERNAL";
options.MetadataAddress = KeycloakUrls.WellKnownConfig(RealmConstants.BCPSRealm, config.Keycloak.RealmUrl);
options.TokenValidationParameters = new TokenValidationParameters()
{
ValidateIssuerSigningKey = true,
ValidateIssuer = false,
ValidateIssuer = true,
ValidIssuer = KeycloakUrls.Authority(RealmConstants.BCPSRealm, config.Keycloak.RealmUrl),
ValidateAudience = false,
ValidAlgorithms = new List<string>() { "RS256" }
ValidAlgorithms = ["RS256"]
};
options.Events = new JwtBearerEvents
{
Expand Down
2 changes: 1 addition & 1 deletion backend/webapi.tests/pidp.tests.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@
<PackageReference Include="Microsoft.AspNetCore.TestHost" Version="8.0.10" />
<PackageReference Include="Microsoft.EntityFrameworkCore.InMemory" Version="8.0.10" />
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.11.1" />
<PackageReference Include="NodaTime" Version="3.1.12" />
<PackageReference Include="NodaTime" Version="3.2.0" />
<PackageReference Include="xunit" Version="2.9.2" />
<PackageReference Include="xunit.runner.visualstudio" Version="2.8.2">
<PrivateAssets>all</PrivateAssets>
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,131 @@
namespace Pidp.Features.DigitalEvidenceCaseManagement.BackgroundServices;

using Common.Kafka;
using Microsoft.EntityFrameworkCore;
using NodaTime;
using Pidp.Data;
using Pidp.Infrastructure.HttpClients.Edt;
using Pidp.Models;
using Prometheus;
using Quartz;

[PersistJobDataAfterExecution]
[DisallowConcurrentExecution]
public class SyncCaseAccessJob(PidpDbContext dbContext,
IKafkaProducer<string, SubAgencyDomainEvent> kafkaProducer,
PidpConfiguration config, ILogger<SyncCaseAccessJob> logger,
IServiceScopeFactory serviceScopeFactory, IClock clock, IEdtCoreClient
coreClient, IEdtCaseManagementClient caseManagementClient) : IJob
{
private const string AUTOSYNC = "AutoSync";
private const string COMPLETE = "Complete";
private const string DELETED = "Deleted";
private const string AUF_TOOLS_CASE = "AUF Tools Case";
private static readonly Counter UpdatedUsersCaseCount = Metrics.CreateCounter("edt_sync_cases_users_total", "Number of users with case sync updates");
private static readonly Histogram UpdatedUsersCaseTiming = Metrics.CreateHistogram("edt_sync_cases_users_timing", "Timing of case syncing");


public async Task Execute(IJobExecutionContext context)
{

logger.LogInformation("Sync case access job started");

// loop through all known agency users
var partyIds = await dbContext.SubmittingAgencyRequests.Select(p => p.PartyId).Distinct().ToListAsync();
using (UpdatedUsersCaseTiming.NewTimer())
{
foreach (var partyId in partyIds)
{
var party = await dbContext.Parties.FirstOrDefaultAsync(p => p.Id == partyId);

if (party == null)
{
logger.LogWarning($"Party {partyId} not found");
continue;
}

logger.LogInformation($"Checking case access for party {partyId} {party.Jpdid}");
// get the edt person

var edtUser = await coreClient.GetUserByKey(party.Jpdid);

if (edtUser == null)
{
logger.LogWarning($"User {party.Jpdid} not found in EDT");
continue;
}
else
{
if (edtUser.IsActive == false)
{
logger.LogWarning($"User {party.Jpdid} is not active in EDT");
continue;
}
logger.LogInformation($"Getting current state of {edtUser.UserName} {edtUser.FullName} {edtUser.Id}");
var cases = await coreClient.GetUserCases(edtUser.Id);

var edtCaseIds = cases.Select(c => c.Id).ToList();

// get all diam cases - we only care about the ones that are not deleted and not created in the last 30 seconds
var knownDIAMCaseIds = dbContext.SubmittingAgencyRequests
.Include(sar => sar.Party)
.Where(sar => sar.Party.Jpdid == edtUser.UserName && sar.RequestStatus != DELETED)
.Where(sar => sar.Created <= clock.GetCurrentInstant().Minus(Duration.FromSeconds(30)))
.Select(c => c.CaseId).ToList();

var casesInEdtNotInDIAM = edtCaseIds.Except(knownDIAMCaseIds).ToList();
var casesInDIAMNotInEdt = knownDIAMCaseIds.Except(edtCaseIds).ToList();

// if no count differences then no changes necessary
if (casesInEdtNotInDIAM.Count == 0 && casesInDIAMNotInEdt.Count == 0)
{
logger.LogInformation($"User {edtUser.UserName} has no changes to case access");
continue;
}

foreach (var edtCaseId in casesInEdtNotInDIAM)
{
logger.LogInformation($"User {edtUser.UserName} has access to case {edtCaseId} that is unknown to DIAM");
// get the case info and add as a requested case
var caseInfo = await caseManagementClient.GetCase(edtCaseId);
if (caseInfo != null)
{
logger.LogInformation($"AutoSync adding case {edtCaseId} {caseInfo.AgencyFileNumber} for user {edtUser.UserName}");
SubmittingAgencyRequest agencyRequest = new SubmittingAgencyRequest()
{
AgencyFileNumber = (caseInfo.Id == config.AUFToolsCaseId) ? AUF_TOOLS_CASE : caseInfo.AgencyFileNumber,
CaseId = edtCaseId,
PartyId = partyId,
Created = clock.GetCurrentInstant(),
RequestedOn = clock.GetCurrentInstant(),
RequestStatus = COMPLETE,
Details = AUTOSYNC
};
var added = dbContext.SubmittingAgencyRequests.AddAsync(agencyRequest);

}
}

foreach (var diamCase in casesInDIAMNotInEdt)
{
logger.LogInformation($"User {edtUser.UserName} has DIAM case {diamCase} but does not have access to case in EDT - flagging deleted");
// remove the case from DIAM
var request = await dbContext.SubmittingAgencyRequests.Include(c => c.Party).Where(c => c.RequestStatus != DELETED && c.DeletedOn == null).FirstOrDefaultAsync(sar => sar.CaseId == diamCase && sar.Party.Jpdid == edtUser.UserName);
if (request != null)
{
request.DeletedOn = clock.GetCurrentInstant();
request.RequestStatus = DELETED;
request.Details = AUTOSYNC;
}
}

var updatedRows = await dbContext.SaveChangesAsync();
logger.LogInformation($"Affected {updatedRows} row(s) for user {edtUser.UserName} during auto-sync");
UpdatedUsersCaseCount.Inc(updatedRows);
}


}
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -5,3 +5,5 @@ namespace Pidp.Infrastructure.HttpClients;
public class ChesClientCredentials : ClientCredentialsTokenRequest { }
public class KeycloakAdministrationClientCredentials : ClientCredentialsTokenRequest { }
public class InternalHttpRequestCredentials : ClientCredentialsTokenRequest { }
public class InternalJustinRequestCredentials : ClientCredentialsTokenRequest { }

Original file line number Diff line number Diff line change
Expand Up @@ -53,7 +53,19 @@ public class EdtCaseManagementClient(HttpClient httpClient, ILogger<EdtCaseManag
return null;
}

return result.Value;
// map the agency file number
if (result.Value != null)
{
var caseInfo = result.Value;
var agencyFileNoField = result.Value.Fields.FirstOrDefault(f => f.Name == "Agency File No.");
if (agencyFileNoField != null)
{
caseInfo.AgencyFileNumber = string.IsNullOrEmpty(agencyFileNoField.Value.ToString()) ? "" : agencyFileNoField.Value.ToString();
}
return caseInfo;
}

return null;

}
}
Loading

0 comments on commit 3e00e24

Please sign in to comment.