CSWorks: web-based industrial automation

Of CSWorks and software development

PostgreSQL support

clock July 29, 2011 14:09 by author Sergey Sorokin
Recently, we have tested CSWorks SQL LiveData provider (version 2.0.4115.0) against PostgreSQL database (connecting using npgsql 2.0.11.91), and it worked fine. Here are the settings we used in the CSWorks.Server.LiveDataService.config.

Provider reference:

<system.data>
  <DbProviderFactories>
    ...
    <!-- Make sure that npgsql and its dependencies are accessible -->
    <remove invariant="npgsql"/>
    <add invariant="npgsql" name="PostreSQL Provider for .NET"
      description=".NET PostreSQL Provider for .NET"
      type="Npgsql.NpgsqlFactory, npgsql, Version=2.0.11.91, Culture=neutral, PublicKeyToken=5d8b90d52f46fda7" />
  </DbProviderFactories>
</system.data>

Data source description:

<sqlLiveDataSource name="Database01" sampleBufferLength="16" sqlProviderInvariantName="npgsql"
  connectionString="User Id=postgres;Password=pg123!;Server=localhost;Port=5432;Database=postgres;"
  updateRate="1000" maxQueryLength="65535" queryDelimiter=";" useZeroAndOneForBoolean="false">

Yes, you can use boolean data type to store discrete values, see our sample PostgreSQL table definition:

CREATE TABLE measurements(
PointName varchar(64) NULL,
Sensor1 int2 NULL,
Sensor2 smallint NULL,
Sensor4 int NULL,
Sensor8 bigint NULL,
Sensor1u int2 NULL,
Sensor2u smallint NULL,
Sensor4u int NULL,
Sensor8u bigint NULL,
Sensor10 float NULL,
Sensor50 varchar(64) NULL,
Sensor100 boolean NULL)
insert into measurements values('Point101', 1,2,4,8,1,2,4,8,8.0,'TestString',true)

Another SQL database under CSWorks' belt!



CSWorks user authorization using third-party access management system - Lab 04

clock October 22, 2010 11:38 by author Sergey Sorokin

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).



Security - access rights management

clock September 7, 2010 16:06 by author Sergey Sorokin

CSWorks provides a mechanism to support LiveData, Alarming and Historical data access authorization, but does not have access rights management functionality. Companies that deploy web-based solutions usually have some access management infrastructure in place, which is either based on Active Directory or integrates with some third-party technologies. And in most cases, IT managers in these companies do not like the idea of having yet another piece of security infrastructure that can be used for SCADA-related authentication tasks only. It's just too much hassle: extra support, one more set of user names and passwords to maintain, extra security risks.

This is why we do not offer our own access rights management subsystem with CSWorks - we want to let IT managers choose the right one. We designed CSWorks in a way it can be integrated with existing security solutions. It's up to the application developer team how CSWorks-powered ASP.NET application authenticates a user and what screens are available to the authenticated user. CSWorks can only enforce some restrictions on data access when a CSWorks-powered client application accesses data - LiveData Alarming, or Historical (see CSWorks documentation for details).

If you are new to the world of the web-based access rights management systems, you probably need something to start with. I would recommend downloading a few products available on the market and getting familiar with them.
PortSight SecureAccess and Atlassian Crowd are good candidates: they both support Active Directory integration, both are .NET friendly, both are mature products, both provide samples for .NET developers. If you decide to use such a product for your CSWorks solution, your application should perform the following tasks.

  1. User authentication. Performed by the selected access right management solution either through API or through a separate web page.
  2. CSWorks web service access authorization. Authorization information is stored in the ASP.NET session state, see CSWorks documentation for details.
  3. ASP.NET page access authorization. Your ASP.NET application should provides access only to the pages that are available to the authenticated user. This can be done by using access right management solution API (direct .NET calls or web service-based).
  4. Silverlight page access authorization. Your Silverlight client application should provides access only to the pages that are available to the authenticated user. This can be done by a variety of methods: web service calls from the client applications or passing startup parameters to the Silverlight engine when starting client application.

Of course, there is a learning curve involved in some cases. Yes, third-party (or custom developed) software is involved. The benefits outweigh the risks though - customers get a robust solution that works in a predictable way and re-uses existing security infrastructure.



Resizable Alarm Summary and Trend

clock April 19, 2010 19:11 by author Sergey Sorokin

Resizable controls - what's the problem?

To save HMI screen space, application developers may want to make some controls resizable. Applying ScaleTransform is not a problem and is a good solution in many cases. See Pipes and Tanks Demo for example - it uses ScaleTransform and redraws screen content when a user changes browser window size.

Unfortunately, ScaleTransform is not an optimal solution when dealing with complex UI elements, especially those containing some amount of text - some pieces may look ugly, some may become too small to be useful. The only solution is to make complex controls able to react to the 'SizeChanged' event and recalculate internal layout at runtime. Latest version of Alarm Summary and Trend Control can do that, and this post shows how application developers can use this functionality.

We will need a container control that lets user change its size, and a piece of code that passes the changes to CSWorks controls. You can develop your own container control (there are a lot of examples available in the web) or use an existing third-party container control for that. I will use RadWindow from Telerik in this example - it serves the purpose quite well.

Resizable Alarm Demo

1. Download Rad Controls for Silverlight trial package from www.telerik.com (DLLs only).
2. Unzip downloaded archive to "C:\Program Files\CSWorks\Demo\Src\tps\Telerik". Telerik DLLs will be unpacked to "C:\Program Files\CSWorks\Demo\Src\tps\Telerik\Binaries\Silverlight\".
3. Open Alarm Demo project ("C:\Program Files\CSWorks\Demo\Src\AlarmDemo\") in Visual Studio.
4. We will use RadWindow control implemented in Telerik.Windows.Controls.Navigation.dll. Add Telerik.Windows.Controls.Navigation.dll and Telerik.Windows.Controls.dll to the reference list for Alarm Demo project.
5. Open Page.xaml file. Add 'telerik' namespace to the UserControl element:

<UserControl ...
  xmlns:telerik="clr-namespace:Telerik.Windows.Controls;assembly=Telerik.Windows.Controls.Navigation"
... >


6. Replace control content with a grid that contains RawWindow that contains CSWorks Alarm Summary, and add SizeChanged event handler for RadWindow:

<Grid>
  <telerik:RadWindow x:Name="radWindow" SizeChanged="OnSizeChanged">
    <telerik:RadWindow.Header>
      <TextBlock Text="Alarm Demo"/>
    </telerik:RadWindow.Header>
    <alarm:AlarmSummary ... />
  </telerik:RadWindow>
</Grid>


7. Open Page.xaml.cs file, implement SizeChanged event handler that adjusts AlarmSummary height and width when RadWindow size is changed: 

private void OnSizeChanged(object sender, SizeChangedEventArgs e)
{
  GeneralTransform gtGlobal = alarmSummary.TransformToVisual(radWindow);
  Point leftTopGlobal = gtGlobal.Transform(new Point(0, 0));

  alarmSummary.Height = radWindow.Height - radWindow.BorderThickness.Bottom - leftTopGlobal.Y - 4.0;
  alarmSummary.Width = radWindow.Width - leftTopGlobal.X * 2;
}


8. Update Loaded event handler so RadWindow shows itself after page is loaded:

private void Page_Loaded(object sender, RoutedEventArgs e)
{
  ...
  radWindow.Show();
}


9. Rebuild the project and run Alarm Demo from Start Menu. Resize RadWindow and watch Alarm Summary changing its layout:

Resizable Trend

 You can do the same to CSWorks Trend control.

1. Open Trend Demo project ("C:\Program Files\CSWorks\Demo\Src\TrendDemo\TrendDemo.Sample.csproj")
2. Add Telerik.Windows.Controls.Navigation.dll and Telerik.Windows.Controls.dll to the reference list.
3. Open MainPage.xaml file. Add 'telerik' namespace as shown above.
4. Change xaml so RadWindow contains TrendControl:

<Grid>
  <telerik:RadWindow x:Name="radWindow" SizeChanged="OnSizeChanged">
    <telerik:RadWindow.Header>
      <TextBlock Text="Trend Demo"/>
    </telerik:RadWindow.Header>
    <t:Trend ...>
      ...
    </t:Trend>
  </telerik:RadWindow>
</Grid>


5. Open MainPage.xanl.cs. Add OnSizeChanged handler:

private void OnSizeChanged(object sender, SizeChangedEventArgs e)
{
  GeneralTransform gtGlobal = _demoTrend.TransformToVisual(radWindow);
  Point leftTopGlobal = gtGlobal.Transform(new Point(0, 0));

  _demoTrend.Height = radWindow.Height - radWindow.BorderThickness.Bottom - leftTopGlobal.Y - 4.0;
  _demoTrend.Width = radWindow.Width - leftTopGlobal.X * 2;
}


6. Show RadWindow on startup:

void MainPage_Loaded(object sender, RoutedEventArgs e)
{
  ...
  radWindow.Show();
}


7. Rebuild the project and run Trend Demo from Start Menu. Resize RadWindow and watch Trend control changing its layout: 




Using third-party controls

clock March 15, 2010 15:47 by author Sergey Sorokin

CSWorks client framework is all about third-party UI components. As of this writing, all major Silverlight component providers have come up with their versions of the Gauge control. In this example, I will show how you can integrate Telerik and/or ComponentOne gauge controls into CSWorks Pipes and Tanks Demo application. The result will look like this:



Here are the steps to make it work.

1. Download Telerik Silverlight controls (dlls only) and ComponentOne Studio Trial Package for Silverlight.
2. Create a folder for third-party controls - C:\Program Files\CSWorks\ThirdParty.
3. Unpack downloaded Telerik archive and install ComponentOne package.
4. Copy Telerik assemblies (Telerik.Windows.Controls.Gauge.dll, Telerik.Windows.Controls.dll, Telerik.Windows.Data.dll) and ComponentOne assemblies (C1.Silverlight.dll, C1.Silverlight.Gauge.dll) to ThirdParty folder.
5. Open C:\Program Files\CSWorks\Demo\Src\PipesAndTanksDemo\PipesAndTanksDemo.Sample.csproj in Visual Studio.
6. Add all copied third party assemblies to the project reference list.
7. Open Page.xaml and add third-party namespaces to the namespace list:

<UserControl x:Class="CSWorks.Client.PipesAndTanksDemo.Page"
  ...
  xmlns:tcontrol="clr-namespace:Telerik.Windows.Controls;assembly=Telerik.Windows.Controls.Gauge"
  xmlns:tgauge="clr-namespace:Telerik.Windows.Controls.Gauges;assembly=Telerik.Windows.Controls.Gauge"
  xmlns:c1gauge="clr-namespace:C1.Silverlight.Gauge;assembly=C1.Silverlight.Gauge"

  ...>


8. Add gauge controls to the page and bind them to LeftPumpSpeed and RightPumpSpeed data items:

<controls:TabItem Header="HMI Controls View">
    <Canvas>
        ...
        <Grid ...>
            ...
            <Border ...>
              ...
            </Border>

            <!-- Telerik Gauge for left pump -->
            <Border Grid.Row="1" Grid.Column="5" Grid.RowSpan="1" Grid.ColumnSpan="3" Background="White" BorderBrush="Black" BorderThickness="1" CornerRadius="5" Padding="5,0,5,0" Margin="0,10,0,-10">
                <StackPanel Orientation="Vertical">
                    <TextBlock Text="Telerik Gauge" Foreground="Black" FontSize="8" HorizontalAlignment="Center"/>
                    <tcontrol:RadGauge x:Name="radGauge" VerticalAlignment="Bottom" >
                        <tgauge:RadialGauge  >
                            <tgauge:RadialScale Min="0" Max="200" LabelRotationMode="None" FontSize="7">
                                <tgauge:RadialScale.Label>
                                    <tgauge:LabelProperties FontSize="4.5"/>
                                </tgauge:RadialScale.Label>
                                <tgauge:RangeList>
                                    <tgauge:RadialRange Min="0" Max="100" StartWidth="0.05" EndWidth="0.05" Background="Green"/>
                                    <tgauge:RadialRange Min="100" Max="160" StartWidth="0.05" EndWidth="0.05" Background="Orange"/>
                                    <tgauge:RadialRange Min="160" Max="200" StartWidth="0.05" EndWidth="0.05" Background="Red" />
                                </tgauge:RangeList>
                                <tgauge:IndicatorList>
                                    <tgauge:Needle Value="{Binding Value, Mode=OneWay, Source={StaticResource LeftPumpSpeed}}"/>
                                </tgauge:IndicatorList>
                            </tgauge:RadialScale>
                        </tgauge:RadialGauge>
                    </tcontrol:RadGauge>
                    <TextBlock Text="rpm" Foreground="White" FontSize="7" HorizontalAlignment="Center" VerticalAlignment="Bottom"  Margin="0,-30,0,30"/>
                </StackPanel>
            </Border>

            <!-- ComponentOne Gauge for right pump -->
            <Border Grid.Row="1" Grid.Column="9" Grid.RowSpan="1" Grid.ColumnSpan="3" Background="White" BorderBrush="Black" BorderThickness="1" CornerRadius="5" Padding="5,0,5,0" Margin="0,10,0,-10">
                <StackPanel Orientation="Vertical">
                    <TextBlock Text="C1 Gauge" Foreground="Black" FontSize="8" HorizontalAlignment="Center"/>
                    <c1gauge:C1RadialGauge  Minimum="0" Maximum="200" Value="{Binding Value, Mode=OneWay, Source={StaticResource RightPumpSpeed}}">
                        <c1gauge:C1GaugeLabel Interval="20" Foreground="Black" Format="#" FontSize="6" FontWeight="200" Location="0.85"/>
                        <c1gauge:C1GaugeRange From="0" To="100"  Fill="Green" Location="0.45"/>
                        <c1gauge:C1GaugeRange From="100" To="160"  Fill="Orange" Location="0.45"/>
                        <c1gauge:C1GaugeRange From="160" To="200"  Fill="Red" Location="0.45"/>
                        <c1gauge:C1GaugeMark Interval="20" />
                    </c1gauge:C1RadialGauge>
                    <TextBlock Text="rpm" Foreground="Black" FontSize="7" HorizontalAlignment="Center" VerticalAlignment="Bottom"  Margin="0,-30,0,30"/>
                </StackPanel>
            </Border>


            <TextBlock Text="All UI elements ..." .../>
          </Grid>
          ...
    </Canvas>
</controls:TabItem>


9. Build the project and start Pipes and Tanks Demo from your start menu. Enjoy professional-looking controls in action.