Menno Voerman · Dec 4, 2023 go to post

Hi Scott,

Would be great if you could post the outcome of the WRC tickey. In the near future we also would like to create a generic backend connection to Epic.

Another part of the backend integration is the check of the JWT. So the JWT should be created at IRIS side with the correct client_id, scoped, audience etc. But on IRIS side you need to create a webpage where the public part of the key is hosted so Epic can check that the JWT is valid:

Menno Voerman · Dec 3, 2023 go to post

Hi Scott,

I think I cannot help you any further but you're specific question but I'm very interested in this one with Epic.

For my understandig, are you trying to connect to an oAuth2 Interconnect instance with IRIS? What sort of client did you register at the appmarket of Epic. Is this backend integration of patient/practitioner?

Menno Voerman · Aug 9, 2023 go to post

Hi Pietro,

Not sure if this will help you but I written a retry mechanisme in the BO with 2 additional settings MaxRetries and RetryWaitSeconds. 

See code below:

Class BO.Rest.PostBodyHeaderAuth Extends EnsLib.REST.Operation
{

Parameter ADAPTER = "EnsLib.HTTP.OutboundAdapter";

Parameter INVOCATION = "Queue";

Property HeaderName As %String(MAXLEN = 1000) [ InitialExpression = "Authorization", Required ];

Property HeaderValue As %String(MAXLEN = 10000) [ InitialExpression = "Bearer", Required ];

Property UseSSL As %Integer [ InitialExpression = 1, Required ];

Property MaxRetries As %Integer [ InitialExpression = 3, Required ];

Property RetryWaitSeconds As %Integer [ InitialExpression = 300, Required ];

Property ContentType As %String(MAXLEN = 300) [ InitialExpression = "application/json; charset=utf-8", Required ];

Parameter SETTINGS = "HeaderName:Auth,HeaderValue:Auth,MaxRetries:Retry,RetryWaitSeconds:Retry,UseSSL:Connection,ContentType:ContentType";

Method PostBodyData(pRequest As BO.Rest.PostBodyHeaderAuth.PostBodyHeaderAuthReq, Output pResponse As BO.Rest.PostBodyHeaderAuth.PostBodyHeaderAuthResp) As %Status
{
      Set tSc	= $$$OK
      
      Set tCounter = 0
      
      While (tCounter <= ..MaxRetries)
      {
        //Create a new %Net.HttpRequest
        Set tHttpRequest	= ##class(%Net.HttpRequest).%New()
            
        //Get the serversetting from the Adapter
        Set tHttpRequest.Server	= ..Adapter.HTTPServer
        Set tHttpRequest.Port		= ..Adapter.HTTPPort
            
        //Set Content-Type
        Set tHttpRequest.ContentType = ..ContentType
      
        //Set Body from Request Object
        Do tHttpRequest.EntityBody.Write(pRequest.Body)
        
        // Set Https Flag
        If (..UseSSL = 1) {
            Set tHttpRequest.Https	= 1
            Set tHttpRequest.SSLConfiguration	= ..Adapter.SSLConfig
        } Else {
            Set tHttpRequest.Https	= 0
        }
          
        //Set the Header value for Authentication
        If ($LENGTH(pRequest.HeaderValue) > 0) {
            Set tSc = tHttpRequest.SetHeader(..HeaderName, pRequest.HeaderValue)
        } Else {
            Set tSc = tHttpRequest.SetHeader(..HeaderName, ..HeaderValue)  
        }
        
        //Set Custom Timeouts from Adapter
        Set tHttpRequest.WriteTimeout = ..Adapter.WriteTimeout
        Set tHttpRequest.Timeout = ..Adapter.ResponseTimeout
        Set tHttpRequest.OpenTimeout  = ..Adapter.ConnectTimeout
            
        If ($LENGTH(pRequest.Url) > 0) { 
            Set tUrl = pRequest.Url
        } Else {
            Set tUrl = ..Adapter.URL
        }
      
        //Create a trace with the complete url
        $$$TRACE(..Adapter.HTTPServer_"/"_tUrl)
            
        //Do the post call
        Set tSc = tHttpRequest.Post(tUrl)
    
        //Create the tHttpResponse object
        #dim tHttpResponse As %Net.HttpResponse
          
        //Set the response object	
        Set tHttpResponse = tHttpRequest.HttpResponse
                  
        //If an technical error has occured
        If ($$$ISERR(tSc)) 
        { 
            $$$TRACE("Error:"_$System.Status.GetErrorText(tSc))
            
        } ElseIf ('$IsObject(tHttpResponse)) 
        {
            $$$TRACE("Invalid HTTP Response Object")
            Set tSc=$$$ERROR($$$EnsErrGeneral, "Invalid HTTP Response Object.")
        } 
        Else 
        {
          ;Build Operation Response
          Set pResponse = ##class(BO.Rest.PostBodyHeaderAuth.PostBodyHeaderAuthResp).%New()
          Set pResponse.HTTPStatusCode = tHttpResponse.StatusCode
          Set pResponse.HTTPStatusLine = tHttpResponse.StatusLine
          
          ;Check the HTTPStatusCode
          Set tSuccess = ##class(Util.Functions).IsSuccess(tHttpResponse.StatusCode)
          
          If (tSuccess) {
            Set tCounter = ..MaxRetries + 1
              Do tHttpResponse.Data.Rewind()
              Set pResponse.HTTPResponseData = tHttpResponse.Data
          } Else {
            $$$TRACE("An error HTTP code has been received. HTTPSCode="_tHttpResponse.StatusCode)  
            Try {
                Do tHttpResponse.Data.Rewind()
                Set pResponse.HTTPResponseData = tHttpResponse.Data
            } Catch exception {}
          }
        }
        
        Set tCounter = tCounter+1
        If (tCounter <= ..MaxRetries) {
            $$$TRACE("Retry: "_tCounter_". Wait for "_..RetryWaitSeconds_" seconds")
            HANG ..RetryWaitSeconds	
        }	  
      }
     return tSc
}
}
Menno Voerman · Aug 8, 2023 go to post

Hi Heloisa,

Thanks, wil go for the until (wait until specific time has exceeded or file has been found)

Menno Voerman · Aug 8, 2023 go to post

Hi Eduard,

I has indeed looking for the Deferrd Response. But I think it's not useful in this case looking at the documentation

Menno Voerman · Aug 8, 2023 go to post

@Guillaume Rongier, I've had some struggles with converting but that was nog due to your functions. 

See a snippet of working code below:

ClassMethod Decrypt(Stream As %Stream.GlobalCharacter, key As %String) As %Stream.GlobalCharacter [ Language = python ]
{
    import iris
    import os
    from cryptography.hazmat.primitives import hashes, padding
    from cryptography.hazmat.primitives.ciphers import Cipher, algorithms, modes
    from cryptography.hazmat.backends import default_backend
    from base64 import b64decode

    def stream_to_string(stream)-> str:
                   string = ""
                   stream.Rewind()
                   while not stream.AtEnd:
                                   string += stream.Read(1024)
                   return string

    def string_to_stream(string:str):
                   stream = iris.cls('%Stream.GlobalCharacter')._New()
                   n = 1024
                   chunks = [string[i:i+n] for i in range(0, len(string), n)]
                   for chunk in chunks:
                                   stream.Write(chunk)
                    return stream

    # Convert the Base64 encoded key to bytes
    key_bytes = b64decode(key)

    cipher_bytes = bytes(stream_to_string(Stream),'iso-8859-1')

    # Extract the IV from the first 16 bytes of the cipher
    iv = cipher_bytes[:16]

    # Create the AES cipher object
    backend = default_backend()
    cipher = Cipher(algorithms.AES(key_bytes), modes.CBC(iv), backend=backend)
    decryptor = cipher.decryptor()

    # Decrypt the data, excluding the IV
    decrypted_bytes = decryptor.update(cipher_bytes[16:]) + decryptor.finalize()

    # Remove padding
    unpadder = padding.PKCS7(algorithms.AES.block_size).unpadder()
    decrypted_data = unpadder.update(decrypted_bytes) + unpadder.finalize()
    
    return string_to_stream(decrypted_data)
  }
Menno Voerman · Jul 17, 2023 go to post

Hello Guillaume,

Thank you for the example code.

Will give it a try later on this week and will post the new code.

Menno

Menno Voerman · Jun 29, 2023 go to post

Hi Alex,

Great, thank you for the quick reply.

I was wrong with my idea. The %FromJSON is not the problem but the way I get it from the DynamicObject.

Menno

Menno Voerman · Jun 29, 2023 go to post

Hi Andy,

I would say it should be enough to set indeed the Windows Service to manual/disabled during the maintenance:

If youre in an mirror situation you could also disable the ISCAgent service, but I would say that the node will never switch because the other service is not up and running:

Menno Voerman · May 10, 2023 go to post

For development VS Code is great. Indent is a lot better and also the syntax checking. One small functionality I'm missing is to open classes fast with a shortcut. We're using HealthConnect and at an item the Class Name is shown in the management portal:

I can copy the class name, go to studio press Ctrl-O and paste the class name:

In VS code I can open also with Ctrl-O but then I need to fill in /HS/FHIRServer/Interop.HTTPOperation.cls

Could be a small improvement?

Menno Voerman · Nov 8, 2022 go to post

Hello Marc,

I saw the same, but after adjusting I get the same error when decrypting (Padding is invalid and cannot be removed)

Latest Objectscript code:

Class TEST.ENCRYPT
{

// Symmetric Keys sample to encrypt
ClassMethod DoAESCBCEncrypt() As %Status
{
	Set key="pZR8qfrz7t47G+dboyJCH4NnJRrF+dJbvxq37y/cLUo="
	Write "Key="_key,!
	Set keyBase64=$SYSTEM.Encryption.Base64Encode(key,1)
	Write "KeyBase64="_keyBase64,!
	
	Set iv=$system.Encryption.GenCryptRand(16)
	Write "IV="_iv,!
	Set ivBase64 = $SYSTEM.Encryption.Base64Encode(iv,1)
	Write "IVBase64="_ivBase64,!
	
	Set text="This is just an encryption test with AES256, blocksize 128, padding PKCS7, mode, CBC with an IV of 16 bytes"
	Write "Plain Text: "_text,!
	
	Set encrypted=$SYSTEM.Encryption.AESCBCEncrypt($zcvt(text,"O","UTF8"),keyBase64,iv)
	Set EncryptedBase64=$SYSTEM.Encryption.Base64Encode(encrypted,1)
	Write "EncryptedBase64: "_EncryptedBase64,!
	
	Set encryptedComplete = ivBase64_EncryptedBase64
	Write "EncryptedBase64WithIV: "_encryptedComplete,!
	
	Set ciphertext = $$$URLENCODE(encryptedComplete)

	write "URL Encoded:"_ciphertext,!
	return $$$OK
}

}
Menno Voerman · Nov 7, 2022 go to post

Hi Julius,

Thank you again. I agree with you that $system.Encryption.GenCryptRand(length) should be a nicer option to generate the random IV.

About the IV, this a random IV to make every message unique. The sender will generate the IV and the receiver gets the first 24 characters and this is the IV and will be used in the decryption.

//get IV
string ivString = deEscape.Substring(0, 24);
Console.WriteLine("IV String: " + ivString);

The IV is used in the decryption. The part after the IV is the part thats need to be decrypted:

string toDecryptWithoutIV = deEscape.Substring(24);
Console.WriteLine("To Decrypt without IV: " + toDecryptWithoutIV);

I can't find what's the problem in Objectscript. So i've switched to Python and in Python it works right away. See example code below:

import base64
from base64 import b64encode
from Crypto.Cipher import AES
from Crypto.Util.Padding import pad
import urllib.parse

keyBase64="pZR8qfrz7t47G+dboyJCH4NnJRrF+dJbvxq37y/cLUo="
key = base64.b64decode(keyBase64)

toEncrypt = "This is just an encryption test with AES256, blocksize 128, padding PKCS7, mode, CBC with an IV of 16 bytes"
toEncryptBytes = str.encode(toEncrypt)

cipher = AES.new(key, AES.MODE_CBC)
ct_bytes = cipher.encrypt(pad(toEncryptBytes, AES.block_size))
iv = b64encode(cipher.iv).decode('utf-8')
ct = b64encode(ct_bytes).decode('utf-8')
complete=iv+ct
complete = urllib.parse.quote(complete)
print(complete)

In our used version of IRIS,  Python is embedded so I think this will be the first functionality where we're going to use Python.

Menno Voerman · Nov 5, 2022 go to post

Hi Julius,

Thank you for your response. I've tried your suggestions. See modified code below:

Class TEST.ENCRYPT
{

// Symmetric Keys sample to encrypt
ClassMethod DoAESCBCEncrypt() As %Status
{
	set key="pZR8qfrz7t47G+dboyJCH4NnJRrF+dJbvxq37y/cLUo="
	set iv=##class(%PopulateUtils).StringMin(16,16)	
	Write "Key="_key,!
	Write "IV="_iv,!
	Set ivBase64 = $SYSTEM.Encryption.Base64Encode(iv,1)
	Write "IVBase64="_ivBase64,!
	
	set text="This is just an encryption test with AES256, blocksize 128, padding PKCS7, mode, CBC with an IV of 16 bytes"
	Write "Plain Text: "_text,!
	
	Set encrypted=$SYSTEM.Encryption.AESCBCEncrypt($zcvt(text,"O","UTF8"),key,iv)
	Set EncryptedBase64=$SYSTEM.Encryption.Base64Encode(encrypted,1)
	Write "EncryptedBase64: "_EncryptedBase64,!
	
	Set encryptedComplete = ivBase64_EncryptedBase64
	Write "EncryptedBase64WithIV: "_encryptedComplete,!
	
	Set ciphertext = $$$URLENCODE(encryptedComplete)

	write "URL Encoded:"_ciphertext,!
	return $$$OK
}

}

Unfortunately when decrypted with the C# code I'm getting the error ''padding is invalid and cannot be removed". Is this about the PaddingMode.PKCS7?

Menno Voerman · Oct 25, 2021 go to post

Hi All,

The  problem is in HS.FHIRServer.RestHandler

 Class HS.FHIRServer.HC.FHIRInteropAdapter Extends HS.FHIRServer.RestHandler
{ Parameter isInteropAdapter As %Boolean = 1; Parameter ServiceConfigName As %String = "InteropService"; }

For some reason its not allowed to send the bearer token with the unauthenticated application:

 // Access token present on unsecure CSP request is invalid. Otherwise, if access
// token found on secure CSP request then add to FHIR request AdditionalInfo for
// later evaluation by the FHIR service.
If '%request.Secure {
If ($ZConvert($Piece(%request.GetCgiEnv("HTTP_AUTHORIZATION")," ",1),"U") = "BEARER") || ($Get(%request.Data("access_token",1)) '= "") {
Set %response.Status = ..#HTTP401UNAUTHORIZED
Return $$$OK
}
Set accessToken = ""
Else {
// InterSystems FHIRServer policy is to NOT allow passing access token in the
// request URL or form encoded body (either can be found in %request.Data).
If $Get(%request.Data("access_token",1)) '= "" {
Set %response.Status = ..#HTTP401UNAUTHORIZED
Return $$$OK
}
Set accessToken = ##class(%SYS.OAuth2.AccessToken).GetAccessTokenFromRequest(.tSC)
$$$ThrowOnError(tSC)
If accessToken '= "" {
Do tRequest.AdditionalInfo.SetAt(accessToken, "USER:OAuthToken")
Do tRequest.AdditionalInfo.SetAt(hsrestconfig.OAuthClientName, "USER:OAuthClient")
}
}

We're gonna discuss the issue with intersystems but it looks like we need custom programming to make this situation work in the new HC version.

Menno Voerman · Sep 21, 2021 go to post

Thank you @Alister Pino and @Julius Kavay for your help.

In this scenario see the final solution below:

ClassMethod MergeDynamicObjects(ObjComplete As %Library.DynamicObject, ObjAddendum As %Library.DynamicObject) [ Final ]
{
  Set obj1 = ObjComplete.%Get("OptOuts")
  Set obj2 = ObjAddendum.%Get("OptOuts")     
  Set OptOuts2Iter = obj2.%GetIterator()
  While OptOuts2Iter.%GetNext(.key , .value ) {
    Do obj1.%Push(value)
  }
  return ObjComplete
}
Menno Voerman · Apr 6, 2021 go to post

Hi @Stefan Wittmann,

I was just curious about this functionality. For a project I need to convert ORU~R01 to MDM~T02 messages. 

For now it's fine to write the DTL by myself (small messages). I would be happy to see this functionality in a further release. It can indeed be very useful with migration projects.

Menno Voerman · Apr 6, 2021 go to post

Hi all,

I was wondering in which version I can find the DTL Generator, or is it still in development?

In version ' IRIS for Windows (x86-64) 2020.1.1 (Build 408U) Sun Mar 21 2021 22:04:53 EDT' I cannot find it.

Menno Voerman · Feb 25, 2021 go to post

Hello Sean,

Great, this helps me a lot. I've got a working situation right now. I think the mistake that I made is that I add .cls after the name of the service.

Thanks.

Menno