Article
· Nov 29, 2022 12m read

IS technologies supporting the Pregnancy Symptoms Tracker app

In this article, I’d like to present details of which technologies we had been using to develop our application for the IRIS for Health Contest:

  • REST API generation from OpenAPI specification
  • Role Based Access Control (RBAC) to protect API and Web Pages
  • InterSystems FHIR Server

ToC:
- Application overview
- REST API generation from OpenAPI specification
- Role Based Access Control (RBAC) to protect API and Web Pages
* Securing REST APIs
* Securing Web Pages
* Creating resources and roles
- InterSystems FHIR Server

Application overview

First let me quickly introduce the application supported by those technologies.

Our application was designed to leverage pregnant women to easily report their symptoms. The application is responsive, so symptoms could be handy reported using a mobile device. Such symptoms are recorded using the FHIR Observation resource in the InterSystems FHIR Server.

Patients and doctors are linked together using regular relational tables, referring to IDs of Patient and Practitioner FHIR resources. So, the application also lets doctors check out which symptoms their patients are reporting, allowing them to quickly respond to eventualities.

The application identifies patients/doctors and controls their access authorization using IRIS resources and roles.

The FHIR resources are accessed by a REST API available to the application frontend. A HTTPS connection is established to the FHIR Server using an API KEY stored in IRIS Interoperability Credentials.

The application web resources are handled by the [IRIS Web Gateway[(https://docs.intersystems.com/irislatest/csp/docbook/DocBook.UI.Page.cls...).

Application general architecture

REST API generation from OpenAPI specification

In the IRIS platform, you can define REST interfaces manually or via an OpenAPI specification.

Using OpenAPI has many advantages, like the design-first approach, easy change tracking, easy documentation, strong model, a lot of tools for designing, mocking, testing and so on.

So, let's focus on how to use IRIS REST Services to generate code from an OpenAPI specification.

First, you have to design your API using OpenAPI. In our case, we used the OpenAPI (Swagger) Editor extension for VS Code. It helps you to create your endpoints and other OpenAPI resources directly in VS Code.

OpenAPI extension for VS Code

After you designed your API using OpenAPI specification, you have to save it in a JSON file. In or case we saved our API in this file

Now, you are set to use IRIS REST Services to generate the code for your API. You have three option for such:

In this article, we are going to use the last one, using the %REST.API class. So, open an IRIS terminal and run the following code:

Set applicationName = "dc.apps.pregsymptracker.restapi"
Set swagger = "/irisrun/repo/src/openapi/pregnancy-symptoms-tracker.json"
ZW ##class(%REST.API).CreateApplication(applicationName, swagger, , .newApplication, .internalError)
ZW newApplication
ZW internalError

The OpenApi specification JSON file location is defined in the swagger parameter.

The applicationName parameter is the package name where IRIS REST Services will store the generated code:

REST API classes generated

Three classes are generated:

  • spec.cls: just a container for the OpenAPI specification; you shouldn’t edit this class

  • impl.cls: the main class which contains the implementation of the methods. This class is meant to be edited by you in order to develop the API logic. Here is a tip: always define names for the methods in OpenAPI using the IRIS extension attribute operationId, like here. If you don’t use this attribute, IRIS will create a method with a random name.

  • disp.cls: the dispatch class, in which you bind a web application in order to publish your REST API in IRIS. Tip: make sure that you are displaying generated items in order to see that class. You can edit this class but it’s not a good idea - let IRIS do that for you.

REST API dispatch class

The last two parameters, newApplication and internalError, are output ones and return if your API was created or updated, and possible errors during parsing OpenAPI and generating the classes, respectively. Just write them out to check out this information.

When you update your OpenAPI specification, you must run the CreateApplication method again in order to update the code. The previous logic code implemented by you in the impl class is kept untouched, and comments are added to the point where IRIS REST Service did modifications.

Role Based Access Control (RBAC) to protect API and Web Pages

As said before, the application has two kinds of users: patients and doctors. So, in order to design the access rules for the application resources among these kinds of users, we used resources and roles.

Users should be assigned to roles, roles have permissions to resources, and resources should be required to access system resources - like REST APIs, for instance.

Securing REST APIs

In the IRIS REST Service, you could specify the privileges needed to access the service, through the x-ISC_RequiredResource attribute, an IRIS extension to OpenAPI. You can specify this attribute to the entire API or to specifics endpoints, like this:

    "paths": {
        "/symptom": {
            "post": {
                "operationId": "PostSymptom",
                "x-ISC_RequiredResource": ["AppSymptoms:write"],
                "description": "Used for patients report their symptoms",
        …
        "/doctor/patients": {
            "get": {
                "operationId": "GetDoctorPatientsList",
                "x-ISC_RequiredResource": ["AppAccessDoctorPatients:read"],
                "description": "Get the patientes of the current logged doctor",
        …

After generating the API classes through the OpenAPI specification - like explained before, you can see how IRIS REST Service implements the x-ISC_RequiredResource constraint in the disp class:

ClassMethod PostSymptom() As %Status
{
    Try {
        Set authorized=0
        Do {
            If '$system.Security.Check("AppSymptoms","write") Quit
            Set authorized=1
        } While 0
        If 'authorized Do ##class(%REST.Impl).%ReportRESTError(..#HTTP403FORBIDDEN,$$$ERROR($$$RESTResource)) Quit
        …
    } Catch (ex) {
        Do ##class(%REST.Impl).%ReportRESTError(..#HTTP500INTERNALSERVERERROR,ex.AsStatus(),$parameter("dc.apps.pregsymptracker.restapi.impl","ExposeServerExceptions"))
    }
    Quit $$$OK
}

For more information on how to use RBAC to protect API, please check out this page

Securing Web Pages

In this application, we used CSP Pages to implement the web application. Despite this technique being considered old school - compared to current SPA one, it still has its merits.

For instance, you can require that users have specific roles to access the page. So, summed to securing REST API endpoints - like seem before, you can define an extra security layer to your application.

When a user logs in the system, IRIS assigns a role to it, if that user has roles assigned to it. Such roles could be accessed in the CSP context - through the $ROLE context variable, and be used to require specific roles assigned to users:

<!-- patient.csp -->
script language="cache" method="OnPreHTTP" arguments="" returntype="%Boolean">
Do ##class(dc.apps.pregsymptracker.util.Util).AssertRole("AppPatient")
Return 1
</script>
<!-- doctor.csp -->
script language="cache" method="OnPreHTTP" arguments="" returntype="%Boolean">
Do ##class(dc.apps.pregsymptracker.util.Util).AssertRole("AppDoctor")
Return 1
</script>
ClassMethod AssertRole(pRole As %String)
{
    If ('$Find($ROLES, pRole)){
        Set %response.Redirect = "NoPrivilegesPage.csp"
    }
}

If the current user doesn't have the role AppPatient when assessing the patient.csp page, IRIS web server will redirect such user to the NoPrivilegesPage.csp page, which presents a message informing the user about the security issue. The same for doctor.cpspage, but now requiring the AppDoctor role.

Note that a user could have both roles, in our case AppPatient and AppDoctor, meaning that such a user is a patient and a doctor at the same time, so being able to access both pages.

Creating resources and roles

You can create resources, roles and assign them to users in the IRIS portal - which is straightforward. But here I’d like to share how to create them programmatically:

ClassMethod CreateResources()
{
    Do ..Log("Creating application resources...")
    Set ns = $NAMESPACE
    Try {
        ZN "%SYS"
        Do $CLASSMETHOD("Security.Resources", "Delete", "AppSymptoms")
        Do $CLASSMETHOD("Security.Resources", "Delete", "AppAccessDoctorPatients")
        $$$TOE(st, $CLASSMETHOD("Security.Resources", "Create", "AppSymptoms", "Patient symptoms", "RWU", ""))
        $$$TOE(st, $CLASSMETHOD("Security.Resources", "Create", "AppAccessDoctorPatients", "Patients access rights", "RWU", ""))
    } Catch(e) {
        ZN ns
        Throw e
    }
    ZN ns
}

ClassMethod CreateRoles()
{
    Do ..Log("Creating application roles...")
    Set ns = $NAMESPACE
    Try {
        ZN "%SYS"
        Do $CLASSMETHOD("Security.Roles", "Delete", "AppPatient")
        Do $CLASSMETHOD("Security.Roles", "Delete", "AppDoctor")
        $$$TOE(st, $CLASSMETHOD("Security.Roles", "Create", "AppPatient", "Application Patient role", "AppSymptoms:RWU", ""))
        $$$TOE(st, $CLASSMETHOD("Security.Roles", "Create", "AppDoctor", "Application Doctor role", "AppSymptoms:RWU,AppAccessDoctorPatients:RWU", ""))
    } Catch(e) {
        ZN ns
        Throw e
    }
    ZN ns
}

ClassMethod CreateUsers()
{
    Do ##class(dc.apps.pregsymptracker.util.Setup).Log("Creating example users...")

    // a patient
    &SQL(drop user MarySmith)
    &SQL(create user MarySmith identified by 'marysmith')
    &SQL(grant %DB_IRISAPP, %DB_IRISAPPSECONDARY, AppPatient to MarySmith)
    &SQL(grant select on schema dc_apps_pregsymptracker_data to MarySmith)

    // another patient
    &SQL(drop user SuzieMartinez)
    &SQL(create user SuzieMartinez identified by 'suziemartinez')
    &SQL(grant %DB_IRISAPP, %DB_IRISAPPSECONDARY, AppPatient to SuzieMartinez)
    &SQL(grant select on schema dc_apps_pregsymptracker_data to SuzieMartinez)

    // a doctor
    &SQL(drop user PeterMorgan)
    &SQL(create user PeterMorgan identified by 'petermorgan')
    &SQL(grant %DB_IRISAPP, %DB_IRISAPPSECONDARY, AppDoctor to PeterMorgan)
    &SQL(grant select on schema dc_apps_pregsymptracker_data to PeterMorgan)

    // a doctor who is also a partient
    &SQL(drop user AnneJackson)
    &SQL(create user AnneJackson identified by 'annejackson')
    &SQL(grant %DB_IRISAPP, %DB_IRISAPPSECONDARY, AppDoctor, AppPatient to AnneJackson)
    &SQL(grant select on schema dc_apps_pregsymptracker_data to AnneJackson)
}

InterSystems FHIR Server

The InterSystems FHIR Server is a service that provides access to FHIR resources in the same way IRIS for Health does, but in the cloud.

Despite FHIR Server allows OAuth2 and access FHIR resources directly in the application using libraries such as SMART on FHIR JavaScript Library, in this application we opted to do a hybrid approach, where we uses the FHIR Server as the main data repository but controlled by metadata stored locally in IRIS.

So, we created a FHIR client which is used by the backend to perform FHIR transactions in the FHIR Server. This client was implemented using the %Net.HttpRequest to perform HTTPS calls to the FHIR Server, using an API KEY generated by the server.

This is the code for the HTTP client, which uses `` to implement basic HTTP verbs:

Class dc.apps.pregsymptracker.restapi.HTTPClient Extends %RegisteredObject
{

Property Request As %Net.HttpRequest;

Property Server As %String;

Property Port As %String;

Property UseHTTPS As %Boolean;

Property SSLConfig As %String;

Property APIKeyCred As %String;

Method CreateRequest()
{
    Set ..Request = ##class(%Net.HttpRequest).%New()
    Set ..Request.Server = ..Server
    Set ..Request.Port = ..Port
    Set ..Request.Https = ..UseHTTPS
    If (..UseHTTPS) {
        Do ..Request.SSLConfigurationSet(..SSLConfig)
    }
}

Method SetHeaders(headers As %DynamicObject)
{
    Set headersIt = headers.%GetIterator()
    While (headersIt.%GetNext(.headerName, .headerValue)) {
        Do ..Request.SetHeader(headerName, headerValue)
    }
}

Method GetApiKeyFromEnsCredentials() As %String
{
    Set apiKeyCred = ..APIKeyCred
    $$$TOE(st, ##class(Ens.Config.Credentials).GetCredentialsObj(.apiKeyCredObj, "", "Ens.Config.Credentials", apiKeyCred))
    Return apiKeyCredObj.Password
}

Method HTTPGet(pPath As %String) As %Net.HttpResponse
{
    Do ..CreateRequest()
    $$$TOE(st, ..Request.Get(pPath))
    Set response = ..Request.HttpResponse
    Return response
}

Method HTTPPost(pPath As %String, pBody As %DynamicObject) As %Net.HttpResponse
{
    Do ..CreateRequest()
    Do ..Request.EntityBody.Clear()
    Do ..Request.EntityBody.Write(pBody.%ToJSON())
    $$$TOE(st, ..Request.Post(pPath))
    Set response = ..Request.HttpResponse
    Return response
}

Method HTTPPut(pPath As %String, pBody As %DynamicObject) As %Net.HttpResponse
{
    Do ..CreateRequest()
    Do ..Request.EntityBody.Clear()
    Do ..Request.EntityBody.Write(pBody.%ToJSON())
    $$$TOE(st, ..Request.Put(pPath))
    Set response = ..Request.HttpResponse
    Return response
}

Method HTTPDelete(pPath As %String) As %Net.HttpResponse
{
    Do ..CreateRequest()
    $$$TOE(st, ..Request.Delete(pPath))
    Set response = ..Request.HttpResponse
    Return response
}

}

And this is the code for the FHIR client, which extends the HTTP client and overrides the CreateRequest method to automatically append the FHIR Server API key to the HTTP call.

Class dc.apps.pregsymptracker.restapi.FHIRaaSClient Extends dc.apps.pregsymptracker.restapi.HTTPClient
{

Method CreateRequest()
{
    Do ##super()
    Do ..SetHeaders({
        "x-api-key" : (..GetApiKeyFromEnsCredentials())
    })
}

}
Discussion (0)1
Log in or sign up to continue