public static RedactProcessor Create(AnonymizerConfigurationManager configuratonManager) { var parameters = configuratonManager.GetParameterConfiguration(); return(new RedactProcessor(parameters.EnablePartialDatesForRedact, parameters.EnablePartialAgesForRedact, parameters.EnablePartialZipCodesForRedact, parameters.RestrictedZipCodeTabulationAreas)); }
public void GivenAConfigWithoutProcessingErrorsField_WhenCreateAnonymizerConfigurationManager_TheFieldShouldBeDefaultAsRaise() { var configFilePath = "./TestConfigurations/configuration-without-processing-error.json"; var configurationManager = AnonymizerConfigurationManager.CreateFromConfigurationFile(configFilePath); Assert.Equal(ProcessingErrorsOption.Raise, configurationManager.Configuration.processingErrors); }
public static DateShiftProcessor Create(AnonymizerConfigurationManager configuratonManager) { var parameters = configuratonManager.GetParameterConfiguration(); return(new DateShiftProcessor(parameters.DateShiftKey, parameters.DateShiftKeyPrefix, parameters.EnablePartialDatesForRedact)); }
public void GivenADateShiftKeyPrefix_WhenSet_DateShiftKeyPrefixShouldBeSetCorrectly(string dateShiftKeyPrefix) { var configFilePath = "./TestConfigurations/configuration-test-sample.json"; var configurationManager = AnonymizerConfigurationManager.CreateFromConfigurationFile(configFilePath); configurationManager.SetDateShiftKeyPrefix(dateShiftKeyPrefix); Assert.Equal(dateShiftKeyPrefix, configurationManager.GetParameterConfiguration().DateShiftKeyPrefix); }
public void GivenAValidConfig_WhenCreateAnonymizerConfigurationManager_ConfigurationShouldBeLoaded(string configFilePath) { var configurationManager = AnonymizerConfigurationManager.CreateFromConfigurationFile(configFilePath); var patientRules = configurationManager.GetPathRulesByResourceType("Patient"); Assert.True(patientRules.Any()); var typeRules = configurationManager.GetTypeRules(); Assert.True(typeRules.Any()); var parameters = configurationManager.GetParameterConfiguration(); Assert.True(!string.IsNullOrEmpty(parameters.DateShiftKey)); }
public static ResourceAnonymizerContext Create(ElementNode root, AnonymizerConfigurationManager configurationManager) { var rules = new List <AnonymizerRule>(configurationManager.GetPathRulesByResourceType(root.InstanceType)); var typeRules = configurationManager.GetTypeRules(); if (typeRules != null && typeRules.Any()) { var rulePaths = rules.Select(rule => rule.Path).ToHashSet(); TransformTypeRulesToPathRules(root, typeRules, rules, rulePaths); } return(new ResourceAnonymizerContext(rules)); }
public void GivenAValidConfig_WhenCreateAnonymizerConfigurationManager_ConfigurationShouldBeLoaded(string configFilePath) { var configurationManager = AnonymizerConfigurationManager.CreateFromConfigurationFile(configFilePath); var fhirRules = configurationManager.FhirPathRules; Assert.True(fhirRules.Any()); fhirRules = configurationManager.FhirPathRules; Assert.Single(configurationManager.FhirPathRules.Where(r => "Patient".Equals(r.ResourceType))); Assert.Single(configurationManager.FhirPathRules.Where(r => "TestResource".Equals(r.ResourceType))); Assert.Single(configurationManager.FhirPathRules.Where(r => string.IsNullOrEmpty(r.ResourceType))); Assert.Single(configurationManager.FhirPathRules.Where(r => "Resource".Equals(r.ResourceType))); var parameters = configurationManager.GetParameterConfiguration(); Assert.True(!string.IsNullOrEmpty(parameters.DateShiftKey)); }
public void GivenATypeRuleContainsAnother_WhenParseRule_NestedOneShouldOverwriteInheritedOne() { AnonymizerConfiguration configuration = new AnonymizerConfiguration() { PathRules = new Dictionary <string, string>(), ParameterConfiguration = new ParameterConfiguration(), TypeRules = new Dictionary <string, string>() { { "Address", "redact" }, { "dateTime", "keep" } } }; AnonymizerConfigurationManager configurationManager = new AnonymizerConfigurationManager(configuration); var context = ResourceAnonymizerContext.Create(TestPatientElementNode(), configurationManager); Assert.Contains("Patient.address", context.PathSet); Assert.Contains("Patient.address.period.start", context.PathSet); Assert.Equal("keep", context.RuleList.First(r => r.Path.Equals("Patient.address.period.start")).Method); Assert.Equal("redact", context.RuleList.First(r => r.Path.Equals("Patient.address")).Method); }
public void GivenConflictTypeRuleAndPathRule_WhenParseRule_TypeRuleShouldBeIgnored() { AnonymizerConfiguration configuration = new AnonymizerConfiguration() { PathRules = new Dictionary <string, string>() { { "Patient.address", "keep" } }, ParameterConfiguration = new ParameterConfiguration(), TypeRules = new Dictionary <string, string>() { { "Address", "redact" } } }; AnonymizerConfigurationManager configurationManager = new AnonymizerConfigurationManager(configuration); var context = ResourceAnonymizerContext.Create(TestPatientElementNode(), configurationManager); Assert.Contains("Patient.address", context.PathSet); Assert.Equal("keep", context.RuleList.First().Method); }
public async Task <IAnonymizer> CreateAnonymizerAsync(string configurationLocation, CancellationToken cancellationToken) { EnsureArg.IsNotNullOrEmpty(configurationLocation, nameof(configurationLocation)); using (Stream stream = new MemoryStream()) { try { await _artifactProvider.FetchAsync(configurationLocation, stream, cancellationToken); stream.Position = 0; } catch (FileNotFoundException ex) { throw new AnonymizationConfigurationNotFoundException(ex.Message, ex); } catch (Exception ex) { _logger.LogError($"Failed to fetch Anonymization configuration file: {configurationLocation}"); throw new AnonymizationConfigurationFetchException(ex.Message, ex); } using (StreamReader reader = new StreamReader(stream)) { string configurationContent = await reader.ReadToEndAsync(); try { var engine = new AnonymizerEngine(AnonymizerConfigurationManager.CreateFromSettingsInJson(configurationContent)); return(new ExportAnonymizer(engine)); } catch (Exception ex) { _logger.LogError($"Failed to parse configuration file: {ex.Message}"); throw new FailedToParseAnonymizationConfigurationException(ex.Message, ex); } } } }
public void GivenATypeRule_WhenParseRule_TransformedPathRuleShouldBeReturned() { AnonymizerConfiguration configuration = new AnonymizerConfiguration() { PathRules = new Dictionary <string, string>(), ParameterConfiguration = new ParameterConfiguration(), TypeRules = new Dictionary <string, string>() { { "Address", "redact" }, { "HumanName", "redact" }, { "dateTime", "dateShift" } } }; AnonymizerConfigurationManager configurationManager = new AnonymizerConfigurationManager(configuration); var context = ResourceAnonymizerContext.Create(TestPatientElementNode(), configurationManager); Assert.Contains("Patient.address", context.PathSet); Assert.Contains("Patient.name", context.PathSet); Assert.Contains("Patient.address.period.start", context.PathSet); Assert.Contains("Patient.identifier.period.start", context.PathSet); }
public void GivenAnInvalidConfig_WhenCreateAnonymizerConfigurationManager_ExceptionShouldBeThrown(string configFilePath) { Assert.Throws <AnonymizerConfigurationErrorsException>(() => AnonymizerConfigurationManager.CreateFromConfigurationFile(configFilePath)); }
public static async Task <IActionResult> Run( [HttpTrigger(AuthorizationLevel.Anonymous, "get", "post", "put", "patch", "delete", Route = "participant/{res}/{id?}")] HttpRequest req, [Blob("%DEIDCONFIG%", FileAccess.Read, Connection = "STORAGEACCT")] CloudBlockBlob deidconfig, ILogger log, ClaimsPrincipal principal, string res, string id) { log.LogInformation("FHIR SecureAccess Function Invoked"); if (!principal.Identity.IsAuthenticated) { return(new ContentResult() { Content = Utils.genOOErrResponse("login", "User is not Authenticated"), StatusCode = (int)System.Net.HttpStatusCode.Unauthorized, ContentType = "application/json" }); } //Is the prinicipal a FHIR Server Administrator ClaimsIdentity ci = (ClaimsIdentity)principal.Identity; bool admin = ci.IsInFHIRRole(Environment.GetEnvironmentVariable("ADMIN_ROLE")); //GET (READ) if (req.Method.Equals("GET")) { if (!admin && !ci.IsInFHIRRole(Environment.GetEnvironmentVariable("READER_ROLE"))) { return(new ContentResult() { Content = Utils.genOOErrResponse("auth-denied", "User/Application must be in a reader role to access"), StatusCode = (int)System.Net.HttpStatusCode.Unauthorized, ContentType = "application/json" }); } } else { //OTHER VERBS ARE WRITER if (!admin && !ci.IsInFHIRRole(Environment.GetEnvironmentVariable("WRITER_ROLE"))) { return(new ContentResult() { Content = Utils.genOOErrResponse("auth-denied", "User/Application must be in a writer role to update"), StatusCode = (int)System.Net.HttpStatusCode.Unauthorized, ContentType = "application/json" }); } } string aadten = ci.Tenant(); string name = principal.Identity.Name; string requestBody = await new StreamReader(req.Body).ReadToEndAsync(); //Get/update/check current bearer token to talk to authemticate to FHIR Server if (_bearerToken == null || FHIRClient.isTokenExpired(_bearerToken)) { lock (_lock) { if (_bearerToken == null || FHIRClient.isTokenExpired(_bearerToken)) { log.LogInformation($"Obtaining new OAUTH2 Bearer Token for access to FHIR Server"); _bearerToken = FHIRClient.GetOAUTH2BearerToken(System.Environment.GetEnvironmentVariable("FS_TENANT_NAME"), System.Environment.GetEnvironmentVariable("FS_RESOURCE"), System.Environment.GetEnvironmentVariable("FS_CLIENT_ID"), System.Environment.GetEnvironmentVariable("FS_SECRET")); } } } //Create User Custom Headers for Audit List <HeaderParm> auditheaders = new List <HeaderParm>(); auditheaders.Add(new HeaderParm("X-MS-AZUREFHIR-AUDIT-USERID", name)); auditheaders.Add(new HeaderParm("X-MS-AZUREFHIR-AUDIT-TENANT", aadten)); auditheaders.Add(new HeaderParm("X-MS-AZUREFHIR-AUDIT-SOURCE", req.HttpContext.Connection.RemoteIpAddress.ToString())); auditheaders.Add(new HeaderParm("X-MS-AZUREFHIR-AUDIT-PROXY", "FHIRProxy-ParticipantPatientRelationship")); //Preserve Relevant FHIR Headers List <HeaderParm> customandrestheaders = new List <HeaderParm>(); foreach (string key in req.Headers.Keys) { string s = key.ToLower(); if (s.Equals("etag")) { customandrestheaders.Add(new HeaderParm(key, req.Headers[key].First())); } else if (s.StartsWith("if-")) { customandrestheaders.Add(new HeaderParm(key, req.Headers[key] .First())); } } //Add User Audit Headers customandrestheaders.AddRange(auditheaders); //Get a FHIR Client so we can talk to the FHIR Server log.LogInformation($"Instanciating FHIR Client Proxy"); FHIRClient fhirClient = new FHIRClient(System.Environment.GetEnvironmentVariable("FS_URL"), _bearerToken); FHIRResponse fhirresp = null; List <string> resourceidentities = new List <string>(); List <string> inroles = ci.Roles(); List <string> fhirresourceroles = new List <string>(); fhirresourceroles.AddRange(Environment.GetEnvironmentVariable("PARTICIPANT_ACCESS_ROLES").Split(",")); fhirresourceroles.AddRange(Environment.GetEnvironmentVariable("PATIENT_ACCESS_ROLES").Split(",")); //Load linked Resource Identifiers for each known role the user is in foreach (string r in inroles) { if (fhirresourceroles.Any(r.Equals)) { fhirresp = fhirClient.LoadResource(r, $"identifier={aadten}|{name}", true, auditheaders.ToArray()); var st = (JObject)fhirresp.Content; if (st != null && ((string)st["resourceType"]).Equals("Bundle")) { JArray entries = (JArray)st["entry"]; foreach (JToken tok in entries) { resourceidentities.Add((string)tok["resource"]["resourceType"] + "/" + (string)tok["resource"]["id"]); } } } } //Proxy the call to the FHIR Server JObject result = null; Dictionary <string, bool> porcache = new Dictionary <string, bool>(); if (req.Method.Equals("GET")) { var qs = req.QueryString.HasValue ? req.QueryString.Value : null; fhirresp = fhirClient.LoadResource(res + (id == null ? "" : "/" + id), qs, false, customandrestheaders.ToArray()); } else { fhirresp = fhirClient.SaveResource(res, requestBody, req.Method, customandrestheaders.ToArray()); } //Fix location header to proxy address if (fhirresp.Headers.ContainsKey("Location")) { fhirresp.Headers["Location"].Value = fhirresp.Headers["Location"].Value.Replace(Environment.GetEnvironmentVariable("FS_URL"), req.Scheme + "://" + req.Host.Value + req.Path.Value.Substring(0, req.Path.Value.IndexOf(res) - 1)); } var fhirstr = fhirresp.Content == null ? "" : (string)fhirresp.Content; //Fix server locations to proxy address fhirstr = fhirstr.Replace(Environment.GetEnvironmentVariable("FS_URL"), req.Scheme + "://" + req.Host.Value + req.Path.Value.Substring(0, req.Path.Value.IndexOf(res) - 1)); result = JObject.Parse(fhirstr); //Role Checks if not Administrator if (!admin && !ci.IsInFHIRRole(Environment.GetEnvironmentVariable("GLOBAL_ACCESS_ROLES"))) { if (((string)result["resourceType"]).Equals("Bundle")) { JArray entries = (JArray)result["entry"]; JArray toremove = new JArray(); for (int i = entries.Count - 1; i >= 0; i--) { if (!IsAParticipantOrPatient((JObject)entries[i]["resource"], fhirClient, resourceidentities, porcache, auditheaders.ToArray())) { entries[i].Remove(); } } } else if (!((string)result["resourceType"]).Equals("OperationalOutcome")) { if (!IsAParticipantOrPatient(result, fhirClient, resourceidentities, porcache, auditheaders.ToArray())) { return(new ContentResult() { Content = Utils.genOOErrResponse("auth-denied", $"Not authorized to access resource:{res + (id == null ? "" : "/" + id)}"), StatusCode = (int)System.Net.HttpStatusCode.Unauthorized, ContentType = "application/json" }); } } } //Add Response from FHIR Server Headers foreach (string key in fhirresp.Headers.Keys) { req.HttpContext.Response.Headers.Remove(key); req.HttpContext.Response.Headers.Add(key, fhirresp.Headers[key].Value); } //DE-ID if (ci.IsInFHIRRole(Environment.GetEnvironmentVariable("DEID_ROLES"))) { if (_deidconfig == null && deidconfig.ExistsAsync().GetAwaiter().GetResult()) { lock (_lock) { if (_deidconfig == null) { log.LogInformation($"Loading de-id config from blob store"); var cs = deidconfig.DownloadTextAsync().GetAwaiter().GetResult(); _deidconfig = AnonymizerConfigurationManager.CreateFromConfigurationString(cs); } } } AnonymizerEngine _engine = new AnonymizerEngine(_deidconfig); var str1 = _engine.AnonymizeJson(result.ToString(Formatting.None)); result = JObject.Parse(str1); } return(new JsonResult(result)); }
// This method gets called by the runtime. Use this method to add services to the container. public void ConfigureServices(IServiceCollection services) { // services.Configure<GzipCompressionProviderOptions>(options => options.Level = CompressionLevel.Fastest); services.AddResponseCompression(options => { options.MimeTypes = ResponseCompressionDefaults.MimeTypes.Concat( new[] { "application/fhir+json" }); }); var apiKey = Configuration.GetValue <string>("ApiKey"); services.AddAuthentication(ApiKeyDefaults.AuthenticationScheme) .AddApiKeyInHeaderOrQueryParams(options => { options.Realm = "FHIR Pseudonymizer"; options.KeyName = "X-Api-Key"; options.IgnoreAuthenticationIfAllowAnonymous = true; options.Events = new ApiKeyEvents { OnValidateKey = ctx => { if (string.IsNullOrWhiteSpace(apiKey) || !apiKey.Equals(ctx.ApiKey, StringComparison.InvariantCulture)) { ctx.ValidationFailed(); return(Task.CompletedTask); } var claims = new[] { new Claim("ApiAccess", "Access to FHIR Pseudonymizer API") }; ctx.Principal = new ClaimsPrincipal(new ClaimsIdentity(claims, ctx.Scheme.Name)); ctx.Success(); return(Task.CompletedTask); } }; }); services.AddHttpClient("gPAS", client => { client.BaseAddress = new Uri(Configuration["gPAS:Url"]); if (Configuration["gPAS:Auth:Basic:Username"] != null) { var basicAuthString = $"{Configuration["gPAS:Auth:Basic:Username"]}:{Configuration["gPAS:Auth:Basic:Password"]}"; var byteArray = Encoding.UTF8.GetBytes(basicAuthString); client.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Basic", Convert.ToBase64String(byteArray)); } }).SetHandlerLifetime(TimeSpan.FromMinutes(5)) .AddPolicyHandler(GetRetryPolicy(Configuration.GetValue <int>("gPAS:RequestRetryCount"))) .UseHttpClientMetrics(); services.AddTransient <IGPasFhirClient, GPasFhirClient>(); AnonymizerEngine.InitializeFhirPathExtensionSymbols(); var anonConfigManager = AnonymizerConfigurationManager.CreateFromYamlConfigFile(Configuration["AnonymizationEngineConfigPath"]); // add the anon config as an additional service to allow mocking it services.AddSingleton(_ => anonConfigManager); services.AddSingleton <IAnonymizerEngine>(sp => { var anonConfig = sp.GetService <AnonymizerConfigurationManager>(); var engine = new AnonymizerEngine(anonConfig); if (!string.IsNullOrWhiteSpace(Configuration["gPAS:Url"])) { var gpasFhirClient = sp.GetService <IGPasFhirClient>(); engine.AddProcessor("pseudonymize", new GPasPseudonymizationProcessor(gpasFhirClient)); } return(engine); }); services.AddSingleton <IDePseudonymizerEngine>(sp => { var anonConfig = sp.GetService <AnonymizerConfigurationManager>(); var engine = new DePseudonymizerEngine(anonConfig); if (!string.IsNullOrWhiteSpace(Configuration["gPAS:Url"])) { var gpasFhirClient = sp.GetService <IGPasFhirClient>(); engine.AddProcessor("pseudonymize", new GPasDePseudonymizationProcessor(gpasFhirClient)); } engine.AddProcessor("encrypt", new DecryptProcessor(anonConfig.GetParameterConfiguration().EncryptKey)); return(engine); }); services.AddRouting(options => options.LowercaseUrls = true); services.AddControllers(options => { options.InputFormatters.Insert(0, new FhirInputFormatter()); options.OutputFormatters.Insert(0, new FhirOutputFormatter()); }); services.AddSwaggerGen(c => { c.SwaggerDoc("v2", new OpenApiInfo { Title = "FHIR Pseudonymizer", Version = "v2" }); // Set the comments path for the Swagger JSON and UI. var xmlFile = $"{Assembly.GetExecutingAssembly().GetName().Name}.xml"; var xmlPath = Path.Combine(AppContext.BaseDirectory, xmlFile); c.IncludeXmlComments(xmlPath); }); services.AddHealthChecks() .AddCheck("live", () => HealthCheckResult.Healthy()); if (Configuration.GetValue <bool>("Tracing:Enabled")) { services.AddOpenTelemetryTracing(builder => { var serviceName = Environment.GetEnvironmentVariable("JAEGER_SERVICE_NAME") ?? Environment.GetEnvironmentVariable("OTEL_SERVICE_NAME") ?? Configuration.GetValue <string>("Tracing:ServiceName"); builder .AddAspNetCoreInstrumentation(o => { o.Filter = (r) => { var ignoredPaths = new[] { "/health", "/ready", "/live", "/fhir/metadata" }; var path = r.Request.Path.Value; return(!ignoredPaths.Any(path.Contains)); }; }) .AddSource(Program.ActivitySource.Name) .AddHttpClientInstrumentation() .SetResourceBuilder(ResourceBuilder.CreateDefault().AddService(serviceName)) .AddJaegerExporter(); }); services.Configure <JaegerExporterOptions>(Configuration.GetSection("Tracing:Jaeger")); } }