Skip to content

Commit

Permalink
Merge pull request #12 from miracum/fix-gpas-fhir-gw-v2-de-pseudonymi…
Browse files Browse the repository at this point in the history
…zation-call

feat: support for gPAS 1.10.2+ FHIR GW de-pseudonymization API
  • Loading branch information
chgl authored May 28, 2021
2 parents 53d2416 + 063e7ae commit b5003c1
Show file tree
Hide file tree
Showing 5 changed files with 76 additions and 28 deletions.
83 changes: 59 additions & 24 deletions src/FhirPseudonymizer/GPasFhirClient.cs
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,9 @@ public GPasFhirClient(ILogger<GPasFhirClient> logger, IHttpClientFactory clientF
SizeLimit = config.GetValue<long>("Cache:SizeLimit")
});

SlidingExpiration = TimeSpan.FromMinutes(Config.GetValue<double>("Cache:SlidingExpirationMinutes", 5));
AbsoluteExpiration = TimeSpan.FromMinutes(Config.GetValue<double>("Cache:AbsoluteExpirationMinutes", 10));

var configGPasVersion = config.GetValue<string>("gPAS:Version");
var supportedGPasVersion = SemVersion.Parse(configGPasVersion);
if (supportedGPasVersion >= SemVersion.Parse("1.10.2"))
Expand All @@ -59,17 +62,16 @@ public GPasFhirClient(ILogger<GPasFhirClient> logger, IHttpClientFactory clientF
private HttpClient Client { get; }
private FhirJsonParser FhirParser { get; } = new();
private FhirJsonSerializer FhirSerializer { get; } = new();
private TimeSpan SlidingExpiration { get; }
private TimeSpan AbsoluteExpiration { get; }

public async Task<string> GetOrCreatePseudonymFor(string value, string domain)
{
return await PseudonymCache.GetOrCreateAsync((value, domain), async entry =>
{
var slidingExpiration = Config.GetValue<double>("Cache:SlidingExpirationMinutes", 5);
var absoluteExpiration = Config.GetValue<double>("Cache:AbsoluteExpirationMinutes", 10);

entry.SetSize(1)
.SetSlidingExpiration(TimeSpan.FromMinutes(slidingExpiration))
.SetAbsoluteExpiration(TimeSpan.FromMinutes(absoluteExpiration));
.SetSlidingExpiration(SlidingExpiration)
.SetAbsoluteExpiration(AbsoluteExpiration);

logger.LogDebug("Getting or creating pseudonym for {value} in {domain}", value, domain);

Expand All @@ -86,40 +88,73 @@ public async Task<string> GetOriginalValueFor(string pseudonym, string domain)
{
return await OriginalValueCache.GetOrCreateAsync((pseudonym, domain), async entry =>
{
var slidingExpiration = Config.GetValue<double>("Cache:SlidingExpirationMinutes", 5);
var absoluteExpiration = Config.GetValue<double>("Cache:AbsoluteExpirationMinutes", 10);

entry.SetSize(1)
.SetSlidingExpiration(TimeSpan.FromMinutes(slidingExpiration))
.SetAbsoluteExpiration(TimeSpan.FromMinutes(absoluteExpiration));

var query = new Dictionary<string, string> { ["domain"] = domain, ["pseudonym"] = pseudonym };
.SetSlidingExpiration(SlidingExpiration)
.SetAbsoluteExpiration(AbsoluteExpiration);

logger.LogDebug("Getting original value for pseudonym {pseudonym} from {domain}", pseudonym, domain);

var response = await Client.GetAsync(QueryHelpers.AddQueryString("$de-pseudonymize", query));
response.EnsureSuccessStatusCode();
var content = await response.Content.ReadAsStringAsync();
var parameters = FhirParser.Parse<Parameters>(content);

var original = parameters.GetSingleValue<FhirString>(pseudonym);
if (original == null)
if (useGpasV2FhirApi)
{
logger.LogWarning("Failed to de-pseudonymize {pseudonym}. Returning original value.", pseudonym);
return pseudonym;
return await GetOriginalValueForV2(pseudonym, domain);
}

return original.Value;
return await GetOriginalValueForV1(pseudonym, domain);
});
}

public async Task<string> GetOriginalValueForV1(string pseudonym, string domain)
{
var query = new Dictionary<string, string> { ["domain"] = domain, ["pseudonym"] = pseudonym };

var response = await Client.GetAsync(QueryHelpers.AddQueryString("$de-pseudonymize", query));
response.EnsureSuccessStatusCode();
var content = await response.Content.ReadAsStringAsync();
var parameters = FhirParser.Parse<Parameters>(content);

var original = parameters.GetSingleValue<FhirString>(pseudonym);
if (original == null)
{
logger.LogWarning("Failed to de-pseudonymize. Returning original value.", pseudonym);
return pseudonym;
}

return original.Value;
}

public async Task<string> GetOriginalValueForV2(string pseudonym, string domain)
{
var parameters = new Parameters()
.Add("target", new FhirString(domain))
.Add("pseudonym", new FhirString(pseudonym));

var parametersBody = FhirSerializer.SerializeToString(parameters);
using var content = new StringContent(parametersBody, Encoding.UTF8, "application/fhir+json");

try
{
var response = await Client.PostAsync("$de-pseudonymize", content);
response.EnsureSuccessStatusCode();

var responseContent = await response.Content.ReadAsStringAsync();
var responseParameters = FhirParser.Parse<Parameters>(responseContent);

return responseParameters.GetSingleValue<FhirString>(pseudonym).Value;
}
catch (Exception exc)
{
logger.LogError(exc, "Failed to de-pseudonymize. Returning original value.");
return pseudonym;
}
}

private async Task<string> GetOrCreatePseudonymForV1(string value, string domain)
{
var query = new Dictionary<string, string> { ["domain"] = domain, ["original"] = value };

// this currently uses a HttpClient instead of the FhirClient to leverage
// Polly's resilience support. Once FhirClient allows for overring the HttpClient,
// we can simplify this code a lot.
// Polly, tracing, and metrics support. Once FhirClient allows for overring the HttpClient,
// we can simplify this code a lot: https://github.com/FirelyTeam/firely-net-sdk/issues/1483
var response = await Client.GetAsync(QueryHelpers.AddQueryString("$pseudonymize-allow-create", query));
response.EnsureSuccessStatusCode();
var content = await response.Content.ReadAsStringAsync();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -111,6 +111,12 @@ public static void AddSecurityTag(this ElementNode node, ProcessResult result)
meta.Security.Add(SecurityLabels.GENERALIZED);
}

if (result.IsPseudonymized && !meta.Security.Any(x =>
string.Equals(x.Code, SecurityLabels.PSEUDED.Code, StringComparison.InvariantCultureIgnoreCase)))
{
meta.Security.Add(SecurityLabels.PSEUDED);
}

var newMetaNode = ElementNode.FromElement(meta.ToTypedElement());
if (metaNode == null)
{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -48,5 +48,12 @@ public static class SecurityLabels
Code = "GENERALIZED",
Display = "exact value is replaced with a general value"
};

public static readonly Coding PSEUDED = new Coding
{
System = "http://terminology.hl7.org/CodeSystem/v3-ObservationValue",
Code = "PSEUDED",
Display = "pseudonymized"
};
}
}
7 changes: 3 additions & 4 deletions src/FhirPseudonymizer/Startup.cs
Original file line number Diff line number Diff line change
Expand Up @@ -79,7 +79,7 @@ public void ConfigureServices(IServiceCollection services)
Convert.ToBase64String(byteArray));
}
}).SetHandlerLifetime(TimeSpan.FromMinutes(5))
.AddPolicyHandler(GetRetryPolicy())
.AddPolicyHandler(GetRetryPolicy(Configuration.GetValue<int>("gPAS:RequestRetryCount")))
.UseHttpClientMetrics();

services.AddTransient<IGPasFhirClient, GPasFhirClient>();
Expand Down Expand Up @@ -175,12 +175,11 @@ public void ConfigureServices(IServiceCollection services)
}
}

private static IAsyncPolicy<HttpResponseMessage> GetRetryPolicy()
private static IAsyncPolicy<HttpResponseMessage> GetRetryPolicy(int retryCount = 3)
{
return HttpPolicyExtensions
.HandleTransientHttpError()
.WaitAndRetryAsync(5, retryAttempt => TimeSpan.FromSeconds(Math.Pow(2,
retryAttempt)));
.WaitAndRetryAsync(retryCount, retryAttempt => TimeSpan.FromSeconds(Math.Pow(2, retryAttempt)));
}

public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
Expand Down
1 change: 1 addition & 0 deletions src/FhirPseudonymizer/appsettings.json
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@
},
"gPAS": {
"Url": "",
"RequestRetryCount": 3,
"Auth": {
"Basic": {
"Username": "",
Expand Down

0 comments on commit b5003c1

Please sign in to comment.