Question Kari Vatjus-Anttila · Jan 25, 2024

Hello,

How can I send a request via a SOCKS5 proxy in IRIS, using, for example, EnsLib.REST.Operation?

Background

I need to access APIs inside my corporate network, to which I don't have direct access from my home office. I've set up a SOCKS5 proxy via SSH on my host machine like this:

ssh -D 9999 server.corporate.com

I can then make requests with curl to the APIs I need:

curl -x socks5h://localhost:9999 https://api.corporate.com/api/some/endpoint

And I receive a response. Simple!

Also, inside the containerized IRIS, I can execute the request:

curl -x socks5h://host.docker.internal:9999 https://api.corporate.com/api/some/endpoint

And I get a response.

However, if I configure the REST Operation, which uses EnsLib.HTTP.OutboundAdapter under the hood, to use the proxy, like this:

image

I receive an error when making the request:

ERROR #5002: ObjectScript error: <WRITE>Send+214^%Net.HttpRequest.1

And the response is empty. I've tried setting the proxy server address to both socks5h://host.docker.internal and just plain host.docker.internal, but it does not make any difference.

I guess my question is: Does IRIS support proxying requests via a SOCKS5 proxy, or is HTTP(S) proxy the only way? Am I missing something here if curl works just fine? I would expect IRIS not to have any problem with it as well.

Thanks for any suggestions.

2
0 319
Question Kari Vatjus-Anttila · Dec 21, 2023

Hello,

I've been running IRIS in a container for a while with the durable %SYS feature. Previously, I was running IRIS 2022.x version and decided to upgrade to 2023.1. During image build, I create some namespaces and install a FHIR repo into one of them using the following script:

Do ##class(HS.FHIRServer.Installer).InstallNamespace()
Do ##class(HS.FHIRServer.Installer).InstallInstance(appKey, strategyClass, metadataPackages)

However, when I updated the container (stopped the existing one, started the new one), I noticed a peculiar error. I couldn't open the FHIR Configuration Management utility anymore as it was pointing to an incorrect location.

In the web applications, I can see that the fhirconfig web application resides in /csp/healthshare/<namespace>/fhirconfig/ URL, but the FHIR config utility points to /csp/<namespace>/fhirconfig/ for some reason.

If I manually enter the URL as /csp/healthshare/<namespace>/fhirconfig/, the utility opens correctly. If I delete the durable directory and start anew, the URLs are working and pointing to /csp/healthshare/<namespace>/fhirconfig/. Deleting durable is not an option.

TL;DR: How do I update the FHIR configuration URLs to point to the correct web application? /csp/<namespace>/fhirconfig/ -> /csp/healthshare/<namespace>/fhirconfig/?

2
0 281
Article Kari Vatjus-Anttila · Oct 20, 2023 11m read

I was attempting to find a solution to grant clients anonymous access to certain API endpoints while securing others within my REST API. However, when defining a Web Application, you can only secure the entire application and not specific parts of it.

I scoured the community for answers but didn't find any exact solutions, except one recommendation to create two separate web applications, one secured and the other unsecured. However, in my opinion, this approach involves too much work and creates unnecessary maintenance overhead. I prefer to develop my APIs spec-first and decide within the specification which endpoints should allow anonymous access and which should not.

In this article, I provide two examples: one for Basic Auth and the other for JWT, which is used in OAuth 2.0 context. If you notice any flaws in these examples, please let me know, and I will make the necessary fixes accordingly.

Prerequisites

First, define a Web Application for your REST API. Configure it for unauthenticated access and specify the required privileges for the application. Specify only the roles and resources necessary for the successful use of the API.

Create a class, for example REST.Utils where you will implement the helper classmethods that verify the credentials.

Class REST.Utils  
{

}

Basic Auth

If you want to secure a endpoint using Basic Auth, use the following method to check if the username/password provided in the HTTP Authorization header has the correct privileges to access the restricted API endpoint.

/// Check if the user has the required permissions.
/// - auth: The Authorization header.
/// - resource: The resource to check permissions for.
/// - permissions: The permissions to check.
/// 
/// Example:
/// > Do ##class(REST.Utils).CheckBasicCredentials(%request.GetCgiEnv("HTTP_AUTHORIZATION", ""), "RESOURCE", "U")
/// 
/// Return: %Status. The status of the check.
ClassMethod CheckBasicCredentials(auth As %String, resource As %String, permissions As %String) As %Status
{
  /// Sanity check the input  
  if (auth = "") {
    Return $$$ERROR($$$GeneralError, "No Authorization header provided")
  }

  /// Check if the auth header starts with Basic  
  if ($FIND(auth, "Basic") > 0) {
    /// Strip the "Basic" part from the Authorization header and remove trailing and leading spaces.  
    set auth = $ZSTRIP($PIECE(auth, "Basic", 2), "<>", "W")
  }

  Set tStatus = $$$OK

  /// Decode the base64 encoded username and password  
  Set auth = $SYSTEM.Encryption.Base64Decode(auth)
  Set username = $PIECE(auth, ":", 1)
  Set password = $PIECE(auth, ":", 2)

  /// Attempt to log in as the user provided in the Authorization header  
  Set tStatus = $SYSTEM.Security.Login(username, password)

  if $$$ISERR(tStatus) {
    Return tStatus
  }

  /// Check if the user has the required permissions  
  Set tStatus = $SYSTEM.Security.CheckUserPermission($USERNAME, resource, permissions)

  /// Return the status. If the user has the required permissions, the status will be $$$OK  
  Return tStatus
}

In the endpoint you want to secure, call the CheckBasicCredentials-method and check the return value. A return value of 0 indicates a failed check. In these cases, we return an HTTP 401 back to the client.

The example below checks that the user has SYSTEM_API resource defined with USE privileges. If it does not, return HTTP 401 to the client. Remember that the API user has to have %Service_Login:USE privilege to be able to use the Security.Login method.

Example

  Set authHeader = %request.GetCgiEnv("HTTP_AUTHORIZATION", "")
  Set tStatus = ##class(REST.Utils).CheckBasicCredentials(authHeader, "SYSTEM_API", "USE")
  if ($$$ISERR(tStatus)) {
    Set %response.Status = 401
    Return
  }
  ... rest of the code

JWT

Instead of using Basic Auth to secure an endpoint, I prefer to use OAuth 2.0 JWT Access Tokens, as they are more secure and provides a more flexible way to define privileges via scopes. The following method checks if the JWT access token provided in the HTTP Authorization header has the correct privileges to access the restricted API endpoint.

/// Check if the supplied JWT is valid.
/// - auth: The Authorization header.
/// - scopes: The scopes that this JWT token should have.
/// - oauthClient: The OAuth client that is used to validate the JWT token. (optional)
/// - jwks: The JWKS used for token signature validation (optional)
/// 
/// Example:
/// > Set token = %request.GetCgiEnv("HTTP_AUTHORIZATION", "")
/// > Do ##class(REST.Utils).CheckJWTCredentials(token, "scope1,scope2")
/// 
/// Return: %Status. The status of the check.
ClassMethod CheckJWTCredentials(token As %String, scopes As %String, oauthClient As %String = "", jwks As %String = "") As %Status
{
  Set tStatus = $$$OK

  /// Sanity check the input  
  if (token = "") {
    Return $$$ERROR($$$GeneralError, "No token provided")
  }
  
  /// Check if the auth header starts with Bearer. Cleanup the token if yes.  
  if ($FIND(token, "Bearer") > 0) {
    /// Strip the "Bearer" part from the Authorization header and remove trailing and leading spaces.  
    set token = $ZSTRIP($PIECE(token, "Bearer", 2), "<>", "W")
  }

  /// Build a list from the string of scopes  
  Set scopes = $LISTFROMSTRING(scopes, ",")

  Set scopeList = ##class(%ListOfDataTypes).%New()
  Do scopeList.InsertList(scopes)

  /// Strip whitespaces from each scope  
  For i=1:1:scopeList.Count() {
    Do scopeList.SetAt($ZSTRIP(scopeList.GetAt(i), "<>", "W"), i)
  }

  /// Decode the token  
  Try {
    Do ..JWTToObject(token, .payload, .header)
  } Catch ex {
    Return $$$ERROR($$$GeneralError, "Not a valid JWT token. Exception code: " _ ex.Code _ ". Status: " _ ex.AsStatus())
  }

  /// Get the epoch time of now
  Set now = $ZDATETIME($h,-2)

  /// Check if the token has expired  
  if (payload.exp < now) {
    Return $$$ERROR($$$GeneralError, "Token has expired")
  }

  Set scopesFound = 0

  /// Check if the token has the required scopes
  for i=1:1:scopeList.Count() {
    Set scope = scopeList.GetAt(i)
    Set scopeIter = payload.scope.%GetIterator()
    While scopeIter.%GetNext(.key, .jwtScope) {
      if (scope = jwtScope) {
        Set scopesFound = scopesFound + 1
      }
    }
  }

  if (scopesFound < scopeList.Count()) {
    Return $$$ERROR($$$GeneralError, "Token does not have the required scopes")
  }

  /// If the token is valid scope-wise and it hasn't expired, check if the signature is valid
  if (oauthClient '= "") {
    /// If we have specified a OAuth client, use that to validate the token signature
    Set result = ##class(%SYS.OAuth2.Validation).ValidateJWT(oauthClient, token, , , , , .tStatus,)
    if ($$$ISERR(tStatus) || result '= 1) {
      Return $$$ERROR($$$GeneralError, "Token failed signature validation")
    }
  } elseif (jwks '= "") {
    /// If we have specified a JWKS, use that to validate the token signature
    Set tStatus = ##class(%OAuth2.JWT).JWTToObject(token,,jwks,,,)
    if ($$$ISERR(tStatus)) {
      Return $$$ERROR($$$GeneralError, "Token failed signature validation. Reason: " _ $SYSTEM.Status.GetErrorText(tStatus))
    }
  }

  Return tStatus
}

/// Decode a JWT token.
/// - token: The JWT token to decode.
/// - payload: The payload of the JWT token. (Output)
/// - header: The header of the JWT token. (Output)
/// 
/// Example:
/// > Set token = %request.GetCgiEnv("HTTP_AUTHORIZATION", "")
/// > Do ##class(REST.Utils).JWTToObject(token, .payload, .header)
/// 
/// Return: %Status. The status of the check.
ClassMethod JWTToObject(token As %String, Output payload As %DynamicObject, Output header As %DynamicObject) As %Status
{
  Set $LISTBUILD(header, payload, sign) = $LISTFROMSTRING(token, ".")

  /// Decode and parse Header  
  Set header = $SYSTEM.Encryption.Base64Decode(header)
  Set header = {}.%FromJSON(header)

  /// Decode and parse Payload  
  Set payload = $SYSTEM.Encryption.Base64Decode(payload)
  Set payload = {}.%FromJSON(payload)

  Return $$$OK
}

Again, in the endpoint you want to secure, call the CheckJWTCredentials-method and check the return value. A return value of 0 indicates a failed check. In these cases, we return an HTTP 401 back to the client.

The example below checks if the token has the scopes scope1 and scope2 defined. If it lacks the required scopes, has expired, or fails signature validation, it returns an HTTP 401 status code to the client.

Example

  Set authHeader = %request.GetCgiEnv("HTTP_AUTHORIZATION", "")
  Set tStatus = ##class(REST.Utils).CheckJWTCredentials(authHeader, "scope1,scope2")
  if ($$$ISERR(tStatus)) {
    Set %response.Status = 401
    Return
  }
  ... rest of the code

Conclusion

Here is the full code for the REST.Utils class. If you have any suggestions on how to improve the code, please let me know. I will update the article accordingly.

One obvious improvement would be to check the JWT signature to make sure it is valid. To be able to do that, you need to have the public key of the issuer.

Class REST.Utils
{
  
/// Check if the user has the required permissions.
/// - auth: The Authorization header contents.
/// - resource: The resource to check permissions for.
/// - permissions: The permissions to check.
/// 
/// Example:
/// > Set authHeader = %request.GetCgiEnv("HTTP_AUTHORIZATION", "")
/// > Do ##class(REST.Utils).CheckBasicCredentials(authHeader, "RESOURCE", "U"))
/// 
/// Return: %Status. The status of the check.  
ClassMethod CheckBasicCredentials(authHeader As %String, resource As %String, permissions As %String) As %Status
{
  Set auth = authHeader

  /// Sanity check the input  
  if (auth = "") {
    Return $$$ERROR($$$GeneralError, "No Authorization header provided")
  }

  /// Check if the auth header starts with Basic  
  if ($FIND(auth, "Basic") > 0) {
    // Strip the "Basic" part from the Authorization header and remove trailing and leading spaces.  
    set auth = $ZSTRIP($PIECE(auth, "Basic", 2), "<>", "W")
  }

  Set tStatus = $$$OK

  Try {
  /// Decode the base64 encoded username and password  
  Set auth = $SYSTEM.Encryption.Base64Decode(auth)
  Set username = $PIECE(auth,":",1)
  Set password = $PIECE(auth,":",2)
  } Catch {
    Return $$$ERROR($$$GeneralError, "Not a valid Basic Authorization header")
  }

  /// Attempt to login as the user provided in the Authorization header  
  Set tStatus = $SYSTEM.Security.Login(username,password)

  if $$$ISERR(tStatus) {
    Return tStatus
  }

  /// Check if the user has the required permissions  
  Set tStatus = $SYSTEM.Security.CheckUserPermission($USERNAME, resource, permissions)

  /// Return the status. If the user has the required permissions, the status will be $$$OK  
  Return tStatus
}

/// Check if the supplied JWT is valid.
/// - auth: The Authorization header.
/// - scopes: The scopes that this JWT token should have.
/// - oauthClient: The OAuth client that is used to validate the JWT token. (optional)
/// - jwks: The JWKS used for token signature validation (optional)
/// 
/// Example:
/// > Set token = %request.GetCgiEnv("HTTP_AUTHORIZATION", "")
/// > Do ##class(REST.Utils).CheckJWTCredentials(token, "scope1,scope2")
/// 
/// Return: %Status. The status of the check.
ClassMethod CheckJWTCredentials(token As %String, scopes As %String, oauthClient As %String = "", jwks As %String = "") As %Status
{
  Set tStatus = $$$OK

  /// Sanity check the input  
  if (token = "") {
    Return $$$ERROR($$$GeneralError, "No token provided")
  }
  
  /// Check if the auth header starts with Bearer. Cleanup the token if yes.  
  if ($FIND(token, "Bearer") > 0) {
    /// Strip the "Bearer" part from the Authorization header and remove trailing and leading spaces.  
    set token = $ZSTRIP($PIECE(token, "Bearer", 2), "<>", "W")
  }

  /// Build a list from the string of scopes  
  Set scopes = $LISTFROMSTRING(scopes, ",")

  Set scopeList = ##class(%ListOfDataTypes).%New()
  Do scopeList.InsertList(scopes)

  /// Strip whitespaces from each scope  
  For i=1:1:scopeList.Count() {
    Do scopeList.SetAt($ZSTRIP(scopeList.GetAt(i), "<>", "W"), i)
  }

  /// Decode the token  
  Try {
    Do ..JWTToObject(token, .payload, .header)
  } Catch ex {
    Return $$$ERROR($$$GeneralError, "Not a valid JWT token. Exception code: " _ ex.Code _ ". Status: " _ ex.AsStatus())
  }

  /// Get the epoch time of now
  Set now = $ZDATETIME($h,-2)

  /// Check if the token has expired  
  if (payload.exp < now) {
    Return $$$ERROR($$$GeneralError, "Token has expired")
  }

  Set scopesFound = 0

  /// Check if the token has the required scopes
  for i=1:1:scopeList.Count() {
    Set scope = scopeList.GetAt(i)
    Set scopeIter = payload.scope.%GetIterator()
    While scopeIter.%GetNext(.key, .jwtScope) {
      if (scope = jwtScope) {
        Set scopesFound = scopesFound + 1
      }
    }
  }

  if (scopesFound < scopeList.Count()) {
    Return $$$ERROR($$$GeneralError, "Token does not have the required scopes")
  }

  /// If the token is valid scope-wise and it hasn't expired, check if the signature is valid
  if (oauthClient '= "") {
    /// If we have specified a OAuth client, use that to validate the token signature
    Set result = ##class(%SYS.OAuth2.Validation).ValidateJWT(oauthClient, token, , , , , .tStatus,)
    if ($$$ISERR(tStatus) || result '= 1) {
      Return $$$ERROR($$$GeneralError, "Token failed signature validation")
    }
  } elseif (jwks '= "") {
    /// If we have specified a JWKS, use that to validate the token signature
    Set tStatus = ##class(%OAuth2.JWT).JWTToObject(token,,jwks,,,)
    if ($$$ISERR(tStatus)) {
      Return $$$ERROR($$$GeneralError, "Token failed signature validation. Reason: " _ $SYSTEM.Status.GetErrorText(tStatus))
    }
  }

  Return tStatus
}


/// Decode a JWT token.
/// - token: The JWT token to decode.
/// - payload: The payload of the JWT token. (Output)
/// - header: The header of the JWT token. (Output)
/// 
/// Example:
/// > Set token = %request.GetCgiEnv("HTTP_AUTHORIZATION", "")
/// > Do ##class(REST.Utils).JWTToObject(token, .payload, .header)
/// 
/// Return: %Status. The status of the check.
ClassMethod JWTToObject(token As %String, Output payload As %DynamicObject, Output header As %DynamicObject) As %Status
{
  Set $LISTBUILD(header, payload, sign) = $LISTFROMSTRING(token, ".")

  /// Decode and parse Header  
  Set header = $SYSTEM.Encryption.Base64Decode(header)
  Set header = {}.%FromJSON(header)

  /// Decode and parse Payload  
  Set payload = $SYSTEM.Encryption.Base64Decode(payload)
  Set payload = {}.%FromJSON(payload)

  Return $$$OK
}
}
7
1 808
Article Kari Vatjus-Anttila · Sep 14, 2023 4m read

Effective documentation is a cornerstone of software development, aiding in code comprehension, maintenance, and collaboration. By harnessing the power of Doxygen and the ObjectScript filter I've created, you can generate rich static documentation from your source code. This approach does not require a running IRIS instance and thus is a good choice in situations when access to IRIS is not possible. Static documentation may be provided to end-users as-is, together with the source code.

Introduction

7
1 592
Question Kari Vatjus-Anttila · Jan 28, 2023

Hello,

I read a great article by @Timothy Leavitt here, where he wrote about unit testing and test coverage with ZPM. I am facing a slight problem and was wondering if someone has some insight into the matter.

I am running my unit tests in the following way with ZPM, as instructed. They work well and test reports are generated correctly. Test coverage is also measured correctly according to the logs. However, even though I instructed ZPM to generate Cobertura-style coverage reports, it is not generating one. When I run the GenerateReport() method manually, the report is generated correctly.

3
1 383
Question Kari Vatjus-Anttila · Apr 6, 2022

Hello,

Recently I have been tinkering with VSCode and ObjectScript extension to connect to my dockerized IRIS instance. I have configured the instance to use Apache as a Web Gateway as per instructions and it has been working well. Currently I'm using a self-signed certificate for the SSL part of the connection. The browser nags about insecure certs when connecting to Management Portal but that's expected. 

However when I try to connect to the instance with VSCode it simply fails with the following error message

11
0 1265
Question Kari Vatjus-Anttila · Feb 25, 2022

Hello,

The title says it all. I’m building an IRIS image with docker-compose using a separate Dockerfile. Pretty straightforward procedure: I import a Installer script inside the container containing a Installer Manifest I defined. Within the manifest, I create a namespace with code and data databases in separate locations. My intention is to keep the code database inside the container, so whenever I build the container, the imported code is replaced. The data, however, should be persistent.

6
0 508
Question Kari Vatjus-Anttila · Jan 19, 2022

Hello,

I have been tinkering with FHIR recently and tried to update the FHIR servers Capability Statement after I made some changes. I updated an OAuth2.Issuer Service Registry entrys URL and needed to update the metadata which the FHIR server sends to the client so they can get the updated URL for the authorization server we use.

However, when I run the Console Setup tool with

do ##class(HS.FHIRServer.ConsoleSetup).Setup()
2
0 469
Question Kari Vatjus-Anttila · Oct 31, 2016

Hi,

I'm new with writing Caché Objectscript so I need some assistance. I have XML which contains information like this:

<?xml version="1.0" encoding="UTF-8"?>
<session>
   <sessionId>124364</sessionId>
   <cabinet>demo</cabinet>
   <eventType>IN</eventType>
   <eventTime>20161006160154</eventTime>
   <login>test</login>
   <loginFirstName>test</loginFirstName>
   <loginLastName>test</loginLastName>
</session>

I have a class representing this object as follows:

5
0 1530