.NET Based OPC UA Client/Server SDK  3.2.2.525
Lesson 9: Setting Access Rights Using RolePermissions

Overview

This example shows how to use RoleConfigurations to set access rights to nodes and events.

Setting access rights is splitt in two parts:

In this lesson, the access rights are set directly in code.

Future UaModeler releases will include the ability to set these directly in the model. Once it is released, a tutorial will be available if you prefer this way of setting the access rights.

Another possibility to assign access rights to nodes by overriding HasAccess methods that are defined at the BaseNodeManager. Please see the UaDemoServer example for a description.

Configure Roles

First create a RoleConfiguration.xml to declare the different roles supported by your server. We add a sample role configuration file to the project.

Example Roles

This example configures 3 different roles:

  • SecurtiyAdmin (joe)
  • Operator (john)
  • Observer (all authenticated users)

A Role in the xml tree has the following structure:

<Role Name="Observer" NodeId="i=15668">
<Identities>
<Identity CriteriaType="AuthenticatedUser" />
</Identities>
</Role>

Set Roles by File

The RoleConfiguration file will be loaded by the application at startup and needs to be set in the applications server configuration.

The role configuration file will be set in the ServerSettings like this:

<Extension>
<ServerSettings xmlns="http://unifiedautomation.com/schemas/2011/12/Application.xsd">
<RoleConfigurationsFilePath>%CommonApplicationData%\unifiedautomation\UaSdkNet\RoleConfiguration\UaGettingStartedRoleConfiguration.xml</RoleConfigurationsFilePath>
</ServerSettings>
</Extension>
Note
By default SecurityAdmins are able to modify the role configurations by calling OPC UA Methods. If the configuration changes, the configuration file is written by the sdk.
When choosing the directory, note that the SDK is able to save role configurations changed at runtime back to file. The application needs write access on this directory.
In our example, the role configuration file is placed in the bin directory next to the executable file. This is done only to reduce the complexity of the example.

Set Roles in Memory

Roles can also be set directly at the ServerManager. Override LoadRoleConfigurations() to set your own RoleConfigurations in code.

protected override UnifiedAutomation.UaSchema.RoleConfigurations LoadRoleConfigurations()
{
var inMemoryRoleConfig = new UnifiedAutomation.UaSchema.RoleConfigurations()
{
NamespaceTable = new string[]
{
"http://opcfoundation.org/UA/"
},
Roles = new RoleType[]
{
new RoleType()
{
Name = "Observer",
NodeId ="i=15668",
Identities = new IdentityType[]
{
new IdentityType()
{
CriteriaType = CriteriaType.AuthenticatedUser
}
}
},
new RoleType()
{
Name = "Operator",
NodeId ="i=15680",
Identities = new IdentityType[]
{
new IdentityType()
{
CriteriaType = CriteriaType.UserName,
Value = "john"
}
}
},
new RoleType()
{
Name = "SecurityAdmin",
NodeId ="i=15704",
Identities = new IdentityType[]
{
new IdentityType()
{
CriteriaType = CriteriaType.UserName,
Value = "joe"
}
}
}
}
};
return inMemoryRoleConfig;
}

Setting Permissions on Nodes

Add RolePermissions to Variable

After configuring the roles, we can set the RolePermissionTypeCollection that stores multible RolePermissionTypes to restrict access to nodes based on these roles. This section shows how to do this for variables. We set the RolePermissions for the variables “Temperature” and “TemperatureSetPoint”. In this example all users shall be able to browse the nodes and read all attributes expect Value and RolePermission. The access to the Value and RolePermission attribute will be restricted.

SecurtiyAdmins shall be able to

  • read the current value and the RolePermission attribute of Temperature.
  • read/write the value and read the RolePermission attribute of TemperatureSetPoint.

Operators shall be able to

  • read the current value and the historical data of Temperature
  • read and write the value of TemperatureSetPoint.

Observers/AuthenticatedUsers shall be able to

  • read the value of Temperature
  • read the value of TemperatureSetPoint.

Anonymous shall only be able to browse the nodes

RolePermissionTypeCollection rolePermissionTypeCollectionTemperature = new RolePermissionTypeCollection()
{
new RolePermissionType()
{
RoleId = ObjectIds.WellKnownRole_Operator,
Permissions = PermissionTypeDataType.Read | PermissionTypeDataType.Browse | PermissionTypeDataType.ReadHistory
},
new RolePermissionType()
{
RoleId = ObjectIds.WellKnownRole_Observer,
Permissions = PermissionTypeDataType.Read | PermissionTypeDataType.Browse
},
new RolePermissionType()
{
RoleId = ObjectIds.WellKnownRole_Anonymous,
Permissions = PermissionTypeDataType.Browse
},
new RolePermissionType()
{
RoleId= ObjectIds.WellKnownRole_SecurityAdmin,
Permissions = PermissionTypeDataType.Read | PermissionTypeDataType.Browse | PermissionTypeDataType.ReadRolePermissions
}
};
RolePermissionTypeCollection rolePermissionTypeCollectionTemperatureSetPoint = new RolePermissionTypeCollection()
{
new RolePermissionType()
{
RoleId = ObjectIds.WellKnownRole_Operator,
Permissions = PermissionTypeDataType.ReadWrite | PermissionTypeDataType.Browse
},
new RolePermissionType()
{
RoleId = ObjectIds.WellKnownRole_Observer,
Permissions = PermissionTypeDataType.Read | PermissionTypeDataType.Browse
},
new RolePermissionType()
{
RoleId = ObjectIds.WellKnownRole_Anonymous,
Permissions = PermissionTypeDataType.Browse
},
new RolePermissionType()
{
RoleId= ObjectIds.WellKnownRole_SecurityAdmin,
Permissions = PermissionTypeDataType.ReadWrite | PermissionTypeDataType.Browse | PermissionTypeDataType.ReadRolePermissions
}
};
NodeId parentId = new NodeId(block.Name, InstanceNamespaceIndex);
SetNodePermissions(
parentId,
new QualifiedName("Temperature", TypeNamespaceIndex),
rolePermissionTypeCollectionTemperature);
SetNodePermissions(
parentId,
new QualifiedName("TemperatureSetPoint", TypeNamespaceIndex),
rolePermissionTypeCollectionTemperatureSetPoint);

If the nodes have NodeHandleType Internal or InternalPolled, there’s nothing else to implement. But our nodes are configured to have the NodeHandleType ExternalPolled. Thus, Read and Write are implemented in the NodeManager and we have to check the access permissions before returning values. We are using the HasAccess methods implemented in the BaseNodeManager to check the user’s roles against the RolePermissions set for the node (see Lesson09NodeManager.cs):

if (CannotPassNodeAccessChecks(context, operationHandles[ii].NodeHandle, UserAccessMask.Read, out StatusCode statusCode))
{
dv = new DataValue(statusCode);
}
else
else if (CannotPassNodeAccessChecks(context, operationHandles[ii].NodeHandle, UserAccessMask.Write, out StatusCode statusCode))
{
error = statusCode;
}

To test the implementation, we connect to the server with UaExpert as anonymous user first. When selecting one of the Temperature variables, we notice in the Attributes window that we’re not allowed to read the Value attribute and the UserAccessLevel is “None”. When changing the user to “john”, the value can be read as the UserAccessLevel is now “CurrentRead, HistoryRead” (see screenshots below).

serverlesson09_variable.png
Figure 9.1: Attributes of variable Temperature when connected as anonymous user (left) and as user john (right)

Add RolePermissions to Method

Setting the RolePermissionTypes of methods is quite similar. We will restrict the access so that Operators will be able to call the methods.

In this case, we only have to assign the RolePermissions. The SDK perfoms all access checks. The following sample code can be found in the file Lesson09Nodemanager.RolePermission.cs

RolePermissionTypeCollection methodRolePermission = new RolePermissionTypeCollection()
{
new RolePermissionType()
{
RoleId = ObjectIds.WellKnownRole_Operator,
},
new RolePermissionType()
{
RoleId = ObjectIds.WellKnownRole_Anonymous,
Permissions = PermissionTypeDataType.Read | PermissionTypeDataType.Browse
}
};
foreach (BlockConfiguration block in m_system.GetBlocks())
{
NodeId parentId = new NodeId(block.Name, InstanceNamespaceIndex);
SetNodePermissions(
parentId,
new QualifiedName("Start", TypeNamespaceIndex),
methodRolePermission);
SetNodePermissions(
parentId,
new QualifiedName("StartWithSetPoint", TypeNamespaceIndex),
methodRolePermission);
SetNodePermissions(
parentId,
new QualifiedName("Stop", TypeNamespaceIndex),
methodRolePermission);
}

To test the implementation, we connect to the server as anonymous user and call the method stop on a controller. The method call will fail with error “BadUserAccessDenied”. After changing to user “john” we are able to successfully call the method.

Add RolePermissions to Alarms and Events

RolePermissions can also be used to control the access to events. In this example we will create a RolePermissionTypeCollection and add it to the UserData of the alarm. When sending the event associated with the alarm, we assign this UserData as RolePermissionTypeCollection to the event. The following sample code can be found in the file Lesson09NodeManager.cs.

var rolePermissions = new RolePermissionTypeCollection()
{
new RolePermissionType()
{
RoleId = ObjectIds.WellKnownRole_Operator,
Permissions = PermissionTypeDataType.Browse | PermissionTypeDataType.Read | PermissionTypeDataType.ReadHistory | PermissionTypeDataType.ReceiveEvents
},
new RolePermissionType()
{
RoleId = ObjectIds.WellKnownRole_Observer,
Permissions = PermissionTypeDataType.Browse | PermissionTypeDataType.Read | PermissionTypeDataType.ReadHistory | PermissionTypeDataType.ReceiveEvents
},
new RolePermissionType()
{
RoleId = ObjectIds.WellKnownRole_Anonymous,
Permissions = PermissionTypeDataType.Browse
}
};
SetNodePermissions(alarmId, rolePermissions, true);
SetNodePermissions(alarm.SourceNode, rolePermissions, false);
alarm.SetRolePermissions(rolePermissions, this);

Again, we will test the implementation using UaExpert. First we connect with UaExpert as anonymous user, create an EventView, and drag and drop the Server object to this EventView. Then we create another session with the server as user john, create another EventView, and again drag and drop the Server object to the EventView.

We now call a method being user john and compare the EventViews (see screenshots below). Both users receive the event indicating that the controller state has changed, but only john is able to receive the event associated with the alarm.

serverlesson09_event.png
Figure 9.2: EventViews of the anonymous user (top) and user john (bottom)

Add ImpersonateUser EventHandler

Use the ImpersonateUser EventHandler to control which clients are allowed connect to a server and which are not.

Define Usernames and Passwords

This example contains two hard coded users:

  • john (password: master)
  • joe (password: god)

Joe will be the SecurtiyAdmin, John will be configured as Operator. All authenticated users will get the Observer role. Users without authentication will be set as anonymous.

Note
For simplicity, the usernames and passwords in this example are specified in code. In real world applications, you should use some kind of database.

Enable Logon with Username and Password

For being able to check access rights, we have to enable UserName tokens in the server configuration. Therefore we extend the ServerSettings in app.config:

<ServerSettings xmlns="http://unifiedautomation.com/schemas/2011/12/Application.xsd">
<UserIdentity>
<EnableAnonymous>true</EnableAnonymous>
<EnableUserName>true</EnableUserName>
</UserIdentity>

Sample Code

The following code snippets can be found in the file GettingStartedServerManager.cs.

this.SessionManager.ImpersonateUser += new ImpersonateEventHandler(SessionManager_ImpersonateUser);
private void SessionManager_ImpersonateUser(Session session, ImpersonateEventArgs args)
{
// allow anonymous log on
AnonymousIdentityToken anonynmousToken = args.NewIdentity as AnonymousIdentityToken;
if (anonynmousToken != null)
{
return;
}
// check for UserName token
UserNameIdentityToken userNameToken = args.NewIdentity as UserNameIdentityToken;
if (userNameToken != null)
{
if (String.IsNullOrEmpty(userNameToken.UserName))
{
args.IdentityValidationError = StatusCodes.BadIdentityTokenInvalid;
return;
}
if (userNameToken.UserName == "john")
{
if (userNameToken.DecryptedPassword != "master")
{
args.IdentityValidationError = StatusCodes.BadUserAccessDenied;
}
return;
}
if (userNameToken.UserName == "joe")
{
if (userNameToken.DecryptedPassword != "god")
{
args.IdentityValidationError = StatusCodes.BadUserAccessDenied;
}
return;
}
return;
}
args.IdentityValidationError = StatusCodes.BadIdentityTokenRejected;
return;
}