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 (FHIRClient.isTokenExpired(_bearerToken)) { lock (_lock) { if (FHIRClient.isTokenExpired(_bearerToken)) { log.LogInformation($"Obtaining new OAUTH2 Bearer Token for access to FHIR Server"); _bearerToken = FHIRClient.GetOAUTH2BearerToken(System.Environment.GetEnvironmentVariable("FS_RESOURCE"), System.Environment.GetEnvironmentVariable("FS_TENANT_NAME"), System.Environment.GetEnvironmentVariable("FS_CLIENT_ID"), System.Environment.GetEnvironmentVariable("FS_SECRET")).GetAwaiter().GetResult(); } } } //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); } var jr = new JsonResult(result); jr.StatusCode = (int)fhirresp.StatusCode; return(jr); }
public static async Task <IActionResult> Run( [HttpTrigger(AuthorizationLevel.Anonymous, "get", "post", "put", "patch", "delete", Route = "fhir/{res?}/{id?}")] HttpRequest req, ILogger log, ClaimsPrincipal principal, string res, string id) { /*The basic FHIR proxy by default only validates that the user principal is authenticated (AuthN). * You can clone this code and add your own authorization logic, pre and post processing logic to fit your business * use cases. This function will accept standard REST Verbs as used by HL7 FHIR * * IMPORTANT: Do not publish this function without Authentication (Easy Auth or APIM) you will compromise your FHIR server! * */ log.LogInformation("FHIR ProxyBase Function Invoked"); if (!principal.Identity.IsAuthenticated) { return(new ContentResult() { Content = "User is not Authenticated", StatusCode = (int)System.Net.HttpStatusCode.Unauthorized }); } /* Load the ClaimsIdentity for use in RBAC based logic */ ClaimsIdentity ci = (ClaimsIdentity)principal.Identity; /* Load the tenant Name from principal */ string aadten = ci.Tenant(); /* Load the principal Name */ string name = principal.Identity.Name; /* Minimum Authorization must be in an access Role Reader, Writer or Admin based on HTTP Verb*/ 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-access", "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-access", "User/Application must be in a writer role to update"), StatusCode = (int)System.Net.HttpStatusCode.Unauthorized, ContentType = "application/json" }); } } /* Load the request contents */ string requestBody = await new StreamReader(req.Body).ReadToEndAsync(); /* Get/update/check current bearer token to authenticate the proxy to the FHIR Server * The following parameters must be defined in environment variables: * To use Manged Service Identity or Service Client: * FS_URL = the fully qualified URL to the FHIR Server * FS_RESOURCE = the audience or resource for the FHIR Server for Azure API for FHIR should be https://azurehealthcareapis.com * To use a Service Client Principal the following must also be specified: * FS_TENANT_NAME = the GUID or UPN of the AAD tenant that is hosting FHIR Server Authentication * FS_CLIENT_ID = the client or app id of the private client authorized to access the FHIR Server * FS_SECRET = the client secret to pass to FHIR Server Authentication */ if (!string.IsNullOrEmpty(System.Environment.GetEnvironmentVariable("FS_RESOURCE")) && FHIRClient.isTokenExpired(_bearerToken)) { lock (_lock) { if (FHIRClient.isTokenExpired(_bearerToken)) { log.LogInformation($"Obtaining new OAUTH2 Bearer Token for access to FHIR Server"); _bearerToken = FHIRClient.GetOAUTH2BearerToken(System.Environment.GetEnvironmentVariable("FS_RESOURCE"), System.Environment.GetEnvironmentVariable("FS_TENANT_NAME"), System.Environment.GetEnvironmentVariable("FS_CLIENT_ID"), System.Environment.GetEnvironmentVariable("FS_SECRET")).GetAwaiter().GetResult(); } } } /* * Create User Custom Headers these headers are passed to the FHIR Server to communicate credentials of the authorized user for each proxy call * this is ensures accruate audit trails for FHIR server access. Note: This headers are honored by the Azure API for FHIR Server */ List <HeaderParm> auditheaders = new List <HeaderParm>(); auditheaders.Add(new HeaderParm("X-MS-AZUREFHIR-AUDIT-USERID", principal.Identity.Name)); auditheaders.Add(new HeaderParm("X-MS-AZUREFHIR-AUDIT-TENANT", ci.Tenant())); auditheaders.Add(new HeaderParm("X-MS-AZUREFHIR-AUDIT-SOURCE", req.HttpContext.Connection.RemoteIpAddress.ToString())); auditheaders.Add(new HeaderParm("X-MS-AZUREFHIR-AUDIT-PROXY", "FHIRProxy-ProxyBase")); /* Preserve FHIR Specific change control headers and include in the proxy call */ 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 instance to talk to the FHIR Server */ log.LogInformation($"Instanciating FHIR Client Proxy"); FHIRClient fhirClient = new FHIRClient(System.Environment.GetEnvironmentVariable("FS_URL"), _bearerToken); FHIRResponse fhirresp = null; /* * TODO: Add your pre-call Filter Logic here * Any custom pre FHIR filtering, security, validations * or transform mappings. */ /* Proxy the call to the FHIR Server */ JObject result = null; 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 str = fhirresp.Content == null ? "" : (string)fhirresp.Content; /* Fix server locations to proxy address */ str = str.Replace(Environment.GetEnvironmentVariable("FS_URL"), req.Scheme + "://" + req.Host.Value + req.Path.Value.Substring(0, req.Path.Value.IndexOf(res) - 1)); result = JObject.Parse(str); /* * TODO: Add your Filter Logic here * Any custom post FHIR filtering or security checks * or transform mappings. */ /* Add Headers from FHIR Server Response */ foreach (string key in fhirresp.Headers.Keys) { req.HttpContext.Response.Headers.Remove(key); req.HttpContext.Response.Headers.Add(key, fhirresp.Headers[key].Value); } var jr = new JsonResult(result); jr.StatusCode = (int)fhirresp.StatusCode; return(jr); }
public static async Task <IActionResult> Run( [HttpTrigger(AuthorizationLevel.Anonymous, "get", "post", "put", "patch", "delete", Route = "fhirproxy/{res?}/{id?}/{hist?}/{vid?}")] HttpRequest req, ILogger log, ClaimsPrincipal principal, string res, string id, string hist, string vid) { if (!Utils.isServerAccessAuthorized(req)) { return(new ContentResult() { Content = Utils.genOOErrResponse("auth-access", req.Headers[Utils.AUTH_STATUS_MSG_HEADER].First()), StatusCode = (int)System.Net.HttpStatusCode.Unauthorized, ContentType = "application/json" }); } if (Utils.UnsupportedCommands(res)) { return(new ContentResult() { Content = Utils.genOOErrResponse("unsupported-cmd", $"The command {res} is not supported via the proxy."), StatusCode = (int)System.Net.HttpStatusCode.BadRequest, ContentType = "application/json" }); } //Load Request Body string requestBody = await new StreamReader(req.Body).ReadToEndAsync(); //Initialize Response FHIRResponse serverresponse = null; //Call Configured Pre-Processor Modules ProxyProcessResult prerslt = await ProxyProcessManager.RunPreProcessors(requestBody, req, log, principal, res, id, hist, vid); if (!prerslt.Continue) { //Pre-Processor didn't like something or exception was called so return FHIRResponse preresp = prerslt.Response; if (preresp == null) { string errmsg = (string.IsNullOrEmpty(prerslt.ErrorMsg) ? "No message" : prerslt.ErrorMsg); FHIRResponse fer = new FHIRResponse(); fer.StatusCode = System.Net.HttpStatusCode.InternalServerError; fer.Content = Utils.genOOErrResponse("internalerror", $"A Proxy Pre-Processor halted execution for an unknown reason. Check logs. Message is {errmsg}"); return(genContentResult(fer, log)); } //Do not continue, so no call to the fhir server go directly to post processing with the response from the pre-preprocessor serverresponse = preresp; goto PostProcessing; } log.LogInformation("Calling FHIR Server..."); //Proxy the call to the FHIR Server serverresponse = await FHIRClientFactory.callFHIRServer(prerslt.Request, req, log, res, id, hist, vid); PostProcessing: //Call Configured Post-Processor Modules ProxyProcessResult postrslt = await ProxyProcessManager.RunPostProcessors(serverresponse, req, log, principal, res, id, hist, vid); if (postrslt.Response == null) { string errmsg = (string.IsNullOrEmpty(postrslt.ErrorMsg) ? "No message" : postrslt.ErrorMsg); postrslt.Response = new FHIRResponse(); postrslt.Response.StatusCode = System.Net.HttpStatusCode.InternalServerError; postrslt.Response.Content = Utils.genOOErrResponse("internalerror", $"A Proxy Post-Processor halted execution for an unknown reason. Check logs. Message is {errmsg}"); } //Reverse Proxy Response postrslt.Response = Utils.reverseProxyResponse(postrslt.Response, req, res); //return ActionResult if (postrslt.Response.StatusCode == HttpStatusCode.NoContent) { return(null); } return(genContentResult(postrslt.Response, log)); }