Introduction

In CSWorks 1.4.3900.0 we have added a possibility to restrict user access to specific data sources, alarm groups and historical data points. Inquiring users will find compelling answers awaiting them in CSWorks documentation that explains new security model in detail. In this article, I will focus on CSWorks integration with a third-party access rights management system. For the purpose of demonstration, I will be using SecureAccess 4.2 by PortSight - .NET security component for user management and application access rights management. This product meets all of the requirements to be used as an underlying access management system:

  • it can be accessed from .NET applications
  • it allows custom privileges like LiveData Read, LiveData Write etc.
  • it allows storing of resource identifiers: strings (data sources) and guids (alarm groups, historical data points)
  • it allows retrieval of all resource identifiers of a specific type (data sources, alarm groups, historical data points) available to a specific user for specific action (read, write, acknowledge)

I will walk through the process of creating a custom authorization provider for CSWorks and using it with Secure Demo application that comes as part of the CSWorks setup.

Setting up SecureAccess

Here are the steps:

  • install SecureAccess on a server computer;
  • provide it with database server access and create a new database using SecureAccess Catalog Manager, call this database "SecureAccess";
  • create the following application hierarchy: Applications->AlarmSecurity->Alarm Groups->Pipeline alarms;
  • create a role "PipelineAdmins", create a user "JohnDoe" and add him to the role;

The following screenshot of the SecureAccess administration panel shows what has been done so far:

Roles

Now make sure that Description field of the application part "PipelineAlarms" contains the id of the alarm group "All Pipes" used by CSWorks Alarm Service and defined in Alarms.xml:

...
<alarmGroup id="{4709B095-BBB6-4e48-97B8-AF15C5F19DD6}" description="All Pipes">
...

The following screenshot displays "PipelineAlarms" details:

Parts

At the level of "AlarmSecurity" application, create custom permission types ReadAlarms and AckAlarms and make sure they are available in "PipelineAlarms" application part:

Permission Types

Now the most important thing: on "PipelineAlarms" application part level, give ReadAlarms and AckAlarms permissions "PipelineAdmins" role. Permission matrix for "PipelineAlarms" will look as follows:

Permission Matrix

Now SecureAccess is ready to perform authorization for users who want to access CSWorks alarms in "All Pipes" alarm group.

Developing authorization provider

Using Microsoft Visual Studio, create a new .NET library CSWorks.Server.SecureAccessAuthorizationProvider.dll that references CSWorks.Server.WebSecurity.dll (it comes as part of the CSWorks setup) and the following SecureAccess libraries:

  • ARDataServices.dll
  • ARObjects.dll
  • ARWebSecurity.dll
  • SecureAccess.dll

Create a class SecureAccessAuthorizationProvider that implements IAuthorizationProvider interface defined in CSWorks.Server.WebSecurity:

using System;
using System.Collections.Generic;
using System.Linq;
using PortSight.SecureAccess.ARObjects;
using CSWorks.Server.Diagnostics;

namespace CSWorks.Server.WebSecurity
{
  public class SecureAccessAuthorizationProvider: IAuthorizationProvider
  {
    public void Open(string connectionString, string userName)
    {
      // Nothing to do
    }

    public void Close()
    {
      // Nothing to do
    }

    public IEnumerable GetPrivilegeGuids(string userName, Privilege privilege)
    {
      List ids = new List();
      if (privilege == Privilege.AlarmRead || privilege == Privilege.AlarmAck)
      {
        IEnumerable alarmGroupAppParts = ARHelper.GetARObjects(ARObjectTypesEnum.ARApplicationPart).OfType().Where(ap => ap.ObjectAlias.StartsWith("AlarmSecurity.AlarmGroups.", StringComparison.InvariantCultureIgnoreCase));
        string secureAccessPrivilegeName = (privilege == Privilege.AlarmRead ? "ReadAlarms" : "AckAlarms");

        // Build a list of all group ids this user is authorized to READ or ACK
        foreach (ARApplicationPart ap in alarmGroupAppParts)
        {
          if (ARHelper.IsAuthorized(userName, ap.ObjectAlias, secureAccessPrivilegeName))
          {
            // Take group id from the app part description
            ids.Add(new Guid(ap.ObjectDescription));
          }
        }
      }
      else
      {
        // Report error: privilege not supported
      }

      return ids;
    }

    public IEnumerable GetPrivilegeStrings(string userName, Privilege privilege)
    {
      throw new NotImplementedException();
    }
  }
}

The idea is very straightforward: when given a user name and alarm privilege, GetPrivilegeGuids() method should return all alarm group ids this user has correspondent access to. GetARObjects() returns all application parts under "AlarmGroups", and IsAuthorized() method filters out those alarm groups that are not accessible.

Using authorization provider with your application

Copy CSWorks.Server.SecureAccessAuthorizationProvider.dll together with all SecureAccess dependencies to the bin folder of SecurityDemo application (CSWorks\Demo\Web\bin) so secure CSWorks web services can access it. Add the following lines to the web.config of SecureAlarmWebService located at CSWorks\Demo\Web\SecureAlarmWebService (make sure you have specified proper SQL Server name or address, SQL Server user name and password, and a valid SecureAccess license key):

<configuration>
  <configSections>
    <sectionGroup name="authorizationProviderConfig">
      <section name="authorizationProviders" type="CSWorks.Server.WebSecurity.AuthorizationProviderConfigurationSection, CSWorks.Server.WebSecurity" />
    </sectionGroup>
    ...
  </configSections>
  <appSettings>
    <add key="SecureAccessConnectionString" value="data source=sqlserver;initial catalog=SecureAccess;user id=sqluser;password=sqlpassword;packet size=4096" />
    <add key="SecureAccessDefaultCulture" value="en-US" />
    <add key="SecureAccessParentFrameName" value="" />
    <add key="SecureAccessLicenseKey" value="XXXX-XXXX-XXXX-XXXX-XXXX" />
    <add key="SecureAccessVirtualPath" value="" />
    <add key="SecureAccessTopRecords" value="1000" />
    <add key="SecureAccessCacheExpiration" value="30" />
    <add key="SecureAccessApplicationAlias" value="SecureAccess" />
    <add key="SecureAccessLogonFormRedirectsTo" value="" />
  </appSettings>
  <authorizationProviderConfig>
    <authorizationProviders activeAuthorizationProvider="secureAccessAuthorizationProvider">
      ...
      <authorizationProvider name="secureAccessAuthorizationProvider" type="CSWorks.Server.WebSecurity.SecureAccessAuthorizationProvider, CSWorks.Server.SecureAccessAuthorizationProvider" connectionString="" allowCaching="true"/>
    </authorizationProviders>
  </authorizationProviderConfig>
  ...
</configuration>

SecureAlarmWebService is ready to use the provider you have just crafted. Now modify SecurityDemo.aspx.cs source file of the SecurityDemo sample application by replacing "demooperator@acme.com" with "johndoe" in the AllowAlarmPipes_Click() method as follows:

namespace CSWorks.Server.SecurityDemoWebApplication.WebApplication
{
  public partial class SecurityDemo : System.Web.UI.Page
  {
    ...

    protected void AllowAlarmPipes_Click(object sender, EventArgs e)
    {
      //System.Web.Security.FormsAuthentication.SetAuthCookie("demooperator@acme.com", true);
      System.Web.Security.FormsAuthentication.SetAuthCookie("johndoe", true);
      Response.Redirect("SecurityDemoAlarm.aspx");
    }

    ...
  }
}

From now on, when a user clicks the "Allow 'All Pipes' alarm group only (no acks)" he/she is impersonated as "John Doe" and gets read/ack access to all alarms in the group "All Pipes". All other alarm groups are not available to this user.

Running the demo

Build updated SecurityDemo application, run it and click the "Allow 'All Pipes' alarm group only (no acks)" button. Make sure that "JohnDoe" user name appears in the message on top of the page and you have read/ack access only to those alarms that belong to "All Pipes" alarm group. You can do that by running "Pipes and tanks" demo application and closing all valves that feed the mixing tank: this eventually will trigger an alarm called "Both lines are empty" in "All Pipes" group, and you will be able to see it and acknowledge it from the Security Demo.

The discussed example deals with CSWorks alarming only, but you can easily do the same for LiveData sources (use data source name for SecureAccess application part description, and implement provider's GetPrivilegeStrings() method) and historical data points (use data point id for SecureAccess application part description, and add some code to GetPrivilegeGuids() method).