C++ UA Server SDK
1.5.1.326
|
The C++ SDK Demo Server is a collection of examples for developing different features of an OPC UA server. It is mainly composed of the final results of the Server Getting Started lessons with instance nodes in the BuildingAutomation folder and the Unified Automation Demo address space with nodes in the Demo folder.
The complete project can be found in Examples → Server Cpp Demo.
In this chapter we describe how to add support for user authentication and authorization to the server. Controlling the access to nodes based on user authorization requires two main steps:
There are several smaller steps necessary to implement user authentication and user authorization. The given sample code for user authentication provides a lot of reusable code. However, user authorization shown in the second part of the example is highly application specific. Thus, only a simplified example is shown.
Files used in this example:
Let’s first look at user authentication. We will show two ways of authentication here:
For both ways, we first need to take the following steps:
The Session class provides a place to store the current user context in a SessionUserContext class. This context holds the current userId, groupId, and a setting for the default permission. The SDK uses the default permission for nodes without explicit access permissions.
UaServerApplicationCallback::createSession() creates a new instance of a UaSession, and UaServerApplicationCallback::logonSessionUser() must be implemented to authenticate the user and to store the user context in the session object. Since the helper class OpcServer is used to implement the main server entry, the callback interface OpcServerCallback must be implemented to provide this functionality. The class MyServerCallback implements the callback in the files myservercallback.h and myservercallback.cpp.
Set the callback interface:
In the UaServerApplicationCallback::logonSessionUser we first handle Username/Password authentication:
This simple example works with username and password coded in the source directly. Of course, in a real server this information will come from another source or component like a configuration file or database.
The following groups are used in the sample:
GroupId | GroupName | Description |
---|---|---|
0 | SecurityAdminGroup | Members of the SecurityAdminGroup can receive audit events and configure certificates. |
0 | ConfigurationAdminGroupId | Members of the ConfigurationAdminGroup can configure the server trace. |
1 | users | Normal users |
2 | operators | System operators |
The following users are used in the sample:
UserId | GroupIds | UserName | Password |
---|---|---|---|
0 | 0 | root | secret |
1 | 1 | joe | god |
2 | 0,1,2 | john | master |
3 | 1,2 | sue | curly |
The following code snippet shows the implementation for user certificate authentication:
We configure a file store for user ceritificates and do a validation of the certificate against that store. If the certificate can not be validated, we copy it into a rejected folder.
The SDK provides the option for a per node configuration of access permissions. The default implementation allows configuring different access permissions for owner, group, and others separately for each node.
The following flags are available to set access permission of a node:
Name | Description |
---|---|
AttributeReadable | read all non value attributes |
Readable | read value attribute |
Writable | write value attribute |
Browseable | browse node |
HistoryReadable | read history values |
HistoryInsert | insert into history |
HistoryModify | modify existing history |
HistoryDelete | delete from history |
EventReadable | receive events from that source |
Executable | execute method call |
AttributeWritable | write non value attributes dependent on write mask |
The example for user authentication is applied to the Unified Automation Demo address space. Depending on the user, the access to nodes in Objects → Demo → 005_AccessRights is limited. You can call the method Objects → Demo → 005_AccessRights → Options → AddAdvancedNodes to expand the address space with additional nodes with specific access permissions.
All SDK interface calls triggered by an OPC UA client service call have the Session object as parameter. If you set a user context in the previous step in logonSessionUser(), the SDK automatically checks the user context to decide whether the requested operation is allowed for the current user or not.
In the DemoServer implementation, the code to set access permission on a per node basis can be found in the method NodeManagerDemo::setPermissionsForLimitedNodes().
An example on how to update variable values is already shown in the building automation server introduced in the Server Getting Started Tutorials. This section describes a different approach.
In this tutorial, the variable value handling is set to UaVariable_Value_Cache to indicate that the value needs to be polled through readValues() if a client is interested in the latest value. The method readValues() is called by the SDK for a Read service call and value change checks for monitored items.
If a server should decide whether a value is checked for changes, the value handling of these variables can be set to UaVariable_Value_CacheIsSource | UaVariable_Value_CacheIsUpdatedOnRequest. In this case, the implementer is informed about changes in monitoring and can implement his or her own logic for checking currently monitored variables for changes. The methods readValues() and writeValues() are still called for Read and Write service calls from clients.
The following code snippets show the differences to the tutorial code and can be found in the following files
Alterations are marked by comments in the following form:
Replace the following line in the file controllerobject.cpp
by this code snippet:
The most important change in nmbuildingautomation.h is to overwrite IOManagerUaNode::variableCacheMonitoringChanged(). This method informs the derived class that the monitoring for a variable has been changed.
In addition, the class is derived from UaThread to implement the sampling in a background worker thread. The other additions, like the method run() or the member variables, are necessary to sample the variables which are active in monitoring.
The new members are initialized in the constructor of the class NmBuildingautomation():
Start the sampling worker thread in NmBuildingautomation::afterStartUp():
Stop the sampling worker thread in NmBuildingAutomation::beforeShutDown():
To configure the sampling in the worker thread we have to implement variableCacheMonitoringChanged(). Based on the UaVariableCache::signalCount() the variable is added to sampling when the first monitored item is created, and removed from sampling when the last monitored item is removed. UaVariableCache::getMinSamplingInterval() returns the shortest sampling interval currently used for the variable. This information is not used in this example but it may be used if variables can be sampled with different rates.
The internal sampling is implemented in the main method of the worker thread:
The demo server contains an example for loading all or part of the address space from a UANodeSet XML file.
The sample code can be found in
The example XML file is located in the directory “bin” of the demo server executable. The file buildingautomationxml.xml contains the same controller objects as the building automation server introduced in the Server Getting Started Tutorials. Some of the sample code to connect the loaded variables to the controller simulation is similar to the code used in the tutorials.
The parser and the necessary classes are initialized and added with the following code in the file servermain.cpp.
The class MyBaseNodeFactory is derived from UaBase::BaseNodeFactory and overwrites the method UaBase::BaseNodeFactory::createVariable(). The factory creates data classes for the OPC UA nodes in the XML file. These data classes are used in a second step to create the nodes in the NodeManager. Overwriting the factory allows the creation of specialized data classes. In the example, the data class MyVariable derived from UaBase::Variable is used to parse the extension in the XML file that contains the user data for the variable.
The class MyNodeManagerNodeSetXmlCreator is used to create specialized NodeManagers for a known namespace in the XML file. In this example the special NodeManager is implemented in the class MyNodeManagerNodeSetXml. This class implements all logic necessary to initialize the controller objects and to implement data access to variable values and methods as well as event and alarm handling.
The demo address space code contains sample code for the handling of alarm objects of different types. The example includes alarm objects which show up as OPC UA nodes in the address space as well as alarm objects not visible in the address space. The latter are working completely without nodes in the address space.
The sample code is contained in the class NodeManagerDemo which can be found in the files
The alarm objects are created and initialized in the method createAlarmNodes(). The objects visible in the address space are also added to the NodeManager. These objects are deleted by the NodeManager at shutdown. In addition this method creates variables used to trigger the alarm states.
The alarm objects without nodes have to be deleted in the destructor of the NodeManagerDemo.
The state of the alarm objects can be activated by writing “true” to the OffNormalAlarm variables or by assigning analog values for the level alarms ranging from 0 to 100. Values below 30 trigger a low alarm. Values above 70 trigger a high alarm. To modify the alarm states on write, the method IOManagerUaNode::afterSetAttributeValue() is overwritten in NodeManagerDemo. This method contains the sample code for changing the alarm states.
To handle alarm acknowledgement the methods EventManagerUaNode::OnAcknowledge and EventManagerUaNode::OnConfirm are overwritten in the class NodeManagerDemo. This works only for alarm objects visible in the address space. To handle the Acknowledge and AddComment methods for alarm objects without nodes, the methods getMethodHandle() and beginCall() are implemented to provide the MethodManager functionality for these alarm objects.
The demo address space code contains sample code for the handling of structured data types. A variable with a nested structure can be found in Objects → BuildingAutomation → ControllerConfigurations. This variable contains a structure with two arrays of structures with the configuration parameters for all controllers.
The sample code can be found in the file nmbuildingautomation.cpp
The enumeration, structured data type nodes, and dictionary are created in the function NmBuildingAutomation::createTypeNodes().
The structure value is filled up in the function NmBuildingAutomation::readValues().
The demo address space code contains sample code for the handling of historical access for events. The area object providing event history can be found in Objects → Server → AreaAirConditioner. This object event notifier attribute indicates availablity of event history.
The sample code can be found in the files
The internal event monitored item is created in the function NmBuildingAutomation::afterStartUp().
The class HistoryManagerCache implements all data and event history functions and data and event historizing. The events and data changes are monitored with internal monitored items and are stored in memory.
The sample code expects that the nodes providing event history are managed by the NodeManager that also manages the HistoryManager. This is necessary to forward HistoryRead requests for a node to the right HistoryManager.
If a node is managed by another NodeManager, the responsible HistoryManager must be registered for the a UaNode or for the whole NodeManager. The class NodeManagerBase provides two options. The first is to use NodeManagerBase::setHistoryManager() to set the default HistoryManager for all UaNodes in the NodeManager. The second option is to set a HistoryManager for single UaNodes using the methods NodeManagerBase::setHistoryManagerForUaNode() and NodeManagerBase::removeHistoryManagerForUaNode().
The following sample code shows how to handle event history for the Server object managed by NodeManagerRoot.
The demo address space code contains sample code for dynamic creation of nodes including model change event. The folder with the functionality can be found in Objects → Demo → 008_DynamicNodes. This folder contains two methods for node creation and deletion, a NodeVersion property and the dynamic node. Only nodes with a NodeVersion property are allowed to fire specific model change events of type GeneralModelChangeEventType. This event indicates the changed nodes with a NodeVersion property and the changes (node added/deleted, reference added/deleted). Since the new node does not have a NodeVersion property, only the added reference from the node (008_DynamicNodes) with NodeVersion property has a new reference contained in the model change event.
Changes of nodes without NodeVersion property can be indicated by using the BaseModelChangeEvent. But this event does not indicate which part of the address space has changed.
The sample code can be found in the files
During start-up of the server the GeneralModelChangeEventType must be created in the event type hierarchy. This can be done with the following code contained in NodeManagerDemo::afterStartUp().
The full sample code for the generation of the model change event can be found in the methods NodeManagerDemo::Demo_DynamicNodes_CreateDynamicNode() and NodeManagerDemo::Demo_DynamicNodes_DeleteDynamicNode(). The following code is the essential part of the event generation.
The sample code creates only one change. If more than one node with a NodeVersion property is affected, all changes should be combined in one event.