High Performance OPC UA Server SDK
1.7.1.383
|
The authorization module is responsible for controlling the access of users to different nodes. It is mostly independent from the Authentication module which verifies user identities. Since version 1.6.0 of the SDK the Rolepermissions as defined in the OPC Specification are implemented for authorization control.
This section gives an overview of the basic authorization concepts in the SDK and the OPC UA Specification before diving deeper into the details in the following sections. It is strongly recommended to also consult the OPC UA Specification, Part 18 on Role-Based Security for a better understanding of Rolepermissions.
The authorization is implemented in the authorization backend, which can be selected through cmake by the option UA_AUTHORIZATION_BACKEND. A backend needs to implement the functions from the frontend src/uaserver/session/authorization/authorization.h and certain types from src/uaserver/session/authorization/authorization_types.h. The frontend functions and the higher level convenience functions outside this module (like node rolepermissions) work independent from the current backend being used. Thus it is preferred to use these functions when possible, however some functionality like role management is backend specific.
The default backend is rolepermissions which is the SDK implementation of UA Rolepermissions and will be described further in the following sections below.
Another included backend is null which implements all the required frontend functions, however these are only dummy implementations, so authorization is effectively disabled and every user has full access to every node. However, Accessrestrictions are implemented outside of the backend and still enforced also when using the null backend.
Roles are used to manage permissions for different users, when a client connects to the server, it is assigned one to multiple roles. The roles are assigned based on the identity the client provides and the mapping rules configured in the server. The mapping rules are defined in the OPC Specification and allow different criteria to map user identities to roles, here is a small example for such a mapping:
Role | CriteriaType | Criteria | Comment |
---|---|---|---|
Anonymous | Anonymous | <none> | Assigns the Anonymous role to all clients with an anonymous token |
Observer | AuthenticatedUser | <none> | Assigns the Observer role to all client with a non-anonymous token |
Engineer | Username | John | Assigns the Engineer role to all clients with a username token and username either John or Sue (and a correct password for that username) |
^ | Username | Sue | ^ |
In this case both the Engineer and Observer role would be assigned to the users John and Sue.
The server exposes its configured roles as Objects in the addressspace at Root->Objects->Server->ServerCapabilities->RoleSet, the Identities Property shows the mapping rules, further Properties show the further configuration of the role. However, the role Objects and/or Properties might only be visible to certain roles and only when using an encrypted connection.
In the above example the name of the role is used as identifier, but to uniquely identify roles in UA every role has a nodeid assigned. The nodeid is also used as identifier for the role Object in the addressspace, the Object's browsename is mostly simply called the role's name in the SDK. When combining parts of roles from different sources, roles with the same nodeid also need to have matching names.
For the individual permissions attached to each node the SDK uses the term Nodepermissions, this is to avoid name clashes with rolepermission types defined in namespace 0 and to have a distinct name rather than the more broadly used term rolepermissions.
The nodepermissions consists of a role and and the permissions for that role in the form of OR-ed bits from ua_permissiontype. An array of these nodepermissions is assigned to each node like this:
Node | Role | Permissions |
---|---|---|
NodeA | Anonymous | BROWSE |
^ | AuthenticatedUser | BROWSE | CALL |
NodeB | Anonymous | BROWSE |
^ | AuthenticatedUser | BROWSE | READ |
^ | Engineer | BROWSE | READ | WRITE |
In contrast to individual nodepermissions, a node may also use the namespace default permissions. These also have the form of nodepermissions and are used for every node which does not have explicit nodepermissions set. In case the namespace default permissions are changed, these are changed for all nodes without individual permissions.
Accessrestrictions can be set on individual nodes to further limit access to that node, e.g. only to clients which use an encrypted connection. Accessrestrictions are OR-ed bits from ua_accessrestrictiontype.
The NodeSet2.xml format allows to specify Accessrestrictions, permissions for each node and namespace default permissions. Roles can also be present as normal Objects of type RoleType below the RoleSet Object. The xml2bin/xml2c tools support all these elements, so generated addressspaces will have these set correctly if the source XML already contains these. Note that even when loading role Objects from XML, the content of their Properties must still configured in one of the ways mentioned below.
The OPC Foundation has defined some roles in namespace 0, these roles are present in every server and are considered well-known. A server application does not need to facilitate them, when not assigning any identities to these roles no client will be able to gain them. However some of the roles might be used by companion specifications and there are three roles of special importance:
The well-known roles are exposed as objects in the addressspace like every other role. As the well-known roles are inside namespace 0, their nodeids are accessible as literals in form of the UA_ID_WELLKNOWNROLE_* defines from src/uabase/identifiers.h.
Roles are exposed as objects in the addressspace, thus the official identifier from a client's point of view for a role is the nodeid of the object. This applies to well-known roles and roles from companion specifications as well as roles specified by the server application itself. However when working with roles inside the server nodeids are not very handy, thus the role management mostly works with the internal role_ids when possible.
The role_id is returned when adding a role with ua_role_add_role or can be found with ua_authorization_find_role_by_nodeid, the reverse operation is ua_authorization_get_role_nodeid. The role_ids are valid for the complete runtime of the server, it is recommended for applications to also use the internal role_ids when doing internal work with roles and permissions.
Roles exists in the SDK both as an internal struct inside the rolepermissions backend with an internal role_id to efficiently compare roles and in the addressspace as Objects of type RoleType. It is possible to create the addressspace Object from the internal struct and vice versa, the function which can do this is ua_role_synchronize_roles. It is called during server initialization and also ensures both of these role representations are consistent.
However some parts of the role are not loaded from the addressspace even if present in the source XML file, the values of the role object's Properties must be configured by one of the two ways described in the following:
The SDK allows to define and configure roles via a dedicated role configuration file, it can be specified in the main configuration file or appconfig with the setting session.authorization_roles_file and will then be loaded automatically at server startup.
For more information on this file see Roles Configuration File, here only a simple example of such a role configuration file is shown:
As the file stores nodeids to identify the roles, first a namespace table is needed:
[nstable] nstable/size = 2 nstable/0/url = http://opcfoundation.org/UA/ nstable/1/url = <server>
The server namespace (1) may change depending on the host the server is running, thus the special placeholder <server> is always mapped to the server namespace.
The roles themselves are in an array with one entry for each role, with each role having an array of identity mapping rules:
[roles] roles/size = 3 roles/0/name = Anonymous roles/0/nodeid = i=15644 roles/0/identities/size = 2 roles/0/identities/0/criteria_type = ANONYMOUS roles/0/identities/1/criteria_type = AUTHENTICATEDUSER roles/1/name = AuthenticatedUser roles/1/nodeid = i=15656 roles/1/identities/size = 1 roles/1/identities/0/criteria_type = AUTHENTICATEDUSER roles/2/name = SomeDemoRole roles/2/nodeid = ns=1;s=DemoRole roles/2/identities/size = 2 roles/2/identities/0/criteria_type = USERNAME roles/2/identities/0/criteria = sue roles/2/identities/1/criteria_type = USERNAME roles/2/identities/1/criteria = john
The nodeids i=15644 and i=15656 are well-known namespace 0 nodeids and the Objects already exist in Opc.Ua.NodeSet2.xml, these entries provide the identity mapping rules allowing the server to actually assign these roles to users. The name would not be strictly required for these two entries, but it provides better readability and it is validated against the browsename of the role Objects to help detect inconsistencies.
The Anonymous role has both the ANONYMOUS and AUTHENTICATEDUSER identity mapping, this gives the Anonymous to every user successfully connecting to the server.
The third role does not exist in the addressspace as Object and is created in the server namespace by the SDK during server startup. The role Object will use up some resources like nodes and strings in the respective namespace, so these extra resources must be allocated in that namespace.
Using the role configuration file is the recommended way for configuring roles, however in some cases file IO may not be possible or wanted, it might as well be desired to modify roles during server runtime. Then the functions provided by the SDK for role management must be used, as the role management is implemented in the backend, these functions can be found in RolePermission Backend.
For simple cases, when the role Objects already exits in the loaded addressspace and have numeric nodeids, like it is the case for the well-known namespace 0 roles, the helper function ua_role_add_numeric_identities can be used to just add the identity mapping rules:
static const struct ua_role_numeric_identity identities[] = { {0, UA_ID_WELLKNOWNROLE_ANONYMOUS, UA_IDENTITYCRITERIATYPE_ANONYMOUS, NULL}, {0, UA_ID_WELLKNOWNROLE_ANONYMOUS, UA_IDENTITYCRITERIATYPE_AUTHENTICATEDUSER, NULL}, {0, UA_ID_WELLKNOWNROLE_AUTHENTICATEDUSER, UA_IDENTITYCRITERIATYPE_AUTHENTICATEDUSER, NULL}, {0, UA_ID_WELLKNOWNROLE_SECURITYADMIN, UA_IDENTITYCRITERIATYPE_USERNAME, "root"}, }; ret = ua_role_add_numeric_identities(identities, countof(identities), false); if (ret != 0) goto error;
When the role needs to be created first, ua_role_add_role must be called, followed by ua_role_set_name. Then the further configuration can be done, like setting the identity mapping rules with ua_role_set_identities. Afterwards it might be necessary to call ua_role_synchronize_roles.
There are quite a lot of NodeSet2.xml files without any permissions specified at all, in this case the rolepermissions backend will use the namespace default permissions for every node. As the default permissions aren't given either, the SDK makes up some arbitrary permissions and uses them as default permissions for that namespace. Thus it is recommended to set explicit default permissions using ua_addressspace_set_default_rolepermissions.
Even though a XML file has permissions for nodes specified, it may not set the default permission or it has default permissions but these are completely unsuitable for the server. Thus it is also for these cases recommended to set the default permissions with ua_addressspace_set_default_rolepermissions.
The essential operations for nodepermissions are available backend independent through node rolepermissions. It has functions to get (ua_node_rp_get) nodepermissions and various setters (like ua_node_rp_set and ua_node_rp_set_ns_default). There are also functions for checking specific permissions for nodes like ua_node_allow_read or ua_node_allow_write, these are used by the SDK during the respective service calls and may also be used by applications when choosing to do own provider implementation certain services. The ua_user_ctx struct needed for these functions is embedded inside the uasession_session struct which is available inside each service call implementation.
Permissions for Methods work a bit different than the usual permissions as to determine whether a method call is allowed the call permission bit of two nodes must be checked. For a method call two nodeids must be provided, the id of the Object and the id of the Method. The Method may be the Method directly below the Object, but it may also be the Method at the Object's typedefinition node, both are equally valid. For the permission check however the permissions of the Object and the Method directly below the Object must have the call permission bit set. So even when the Method at the Object's typedefinition is provided by the client, the server still needs to check the permission at the Object's own Method.
This lookup of the call permission bits for the correct nodes is implemented by uaserver_call_utility_check_permission. When using the SDK's default call service implementation this function is called automatically, applications doing their own implementation of the call service may call this function manually or implement that logic on their own.
Events can also only be received when the receive events bit of two nodes allow it: the event type and the source node of the event. Both are given when creating an event with uaserver_event_create and the SDK does check the receive events bit before sending the event to the client. The eventing logic is completely implemented inside the SDK, so there is nothing an application has to do.
The configuration of roles and permissions can quickly get rather complex, so this section helps finding out permissions of nodes and which roles are being assigned to users.
The permission configured for each node are visible through the UAExpert in the respective attributes, this section provides help understanding the attributes. Some of them can change with the current user being connected to the server, so it might be necessary to change the user, this can be done through the Server->Change User dialog. If a node has accessrestrictions set it might also be necessary to use an encrypted connection to see the node or certain attributes.
RolePermissions: The RolePermissions attribute shows all the permission configured for the selected node, however the array may also be empty, that means the node has no individual permissions configured and uses the namespace default permissions.
This attribute has the same value for every user, but access is usually limited, so if BadUserAccessDenied is shown a user with more privileges is needed, probably a member of the role SecurityAdmin. The access to this attribute is controlled by its own permission bit: ReadRolePermissions
UserRolePermissions: The UserRolePermissions attribute shows the subset from RolePermissions which is valid for the current user, so this attribute shows the permissions effectively used for the particular user.
This attribute always shows the permissions, even when the RolePermissions attribute uses the defaults and is empty, thus when the attribute is empty the user actually has zero permissions for this node (in this case however access to the attribute would be denied). Access to this attribute is usually not specially limited, it is controlled by the Browse permission bit like the other regular node attributes.
Default Permissions: Each namespace of the server exposes its DefaultRolePermissions and DefaultUserRolePermissions as Properties of the respective namespace Object at Root->Objects->Server->Namespaces.
In contrast to the above, the relevant data is located in the value attribute of the Properties. These values act like the RolePermissions and UserRolePermissions above, but show the default permissions for the respective namespace instead of just a single node. Also the special meaning of empty does not exists for the DefaultRolePermissions as there is no fallback to defaults.
One more note: When it is not possible to find out the role required to access certain nodes, the SDK allows to bypass the permission check for certain roles by enabling the option ignore_permissions for a role:
roles/X/ignore_permissions = true
When connecting with a member of such a role and having encryption enabled, the client has full access to every attribute of every node in the addressspace, no matter what permissions or restrictions are configured. Of course this option is a last resort and should only be used for finding permission issues, it is not recommended for production use.
To find out which roles are being assigned to a user there are two options:
Addressspace: This is probably the easier option when connected to the server with the UAExpert anyway, but it also gives rather limited information.
The UserRolePermissions attribute shows the subset of the RolePermissions which apply to the current user. So the user is member of every role listed in the UserRolePermissions, however it shows only roles also listed in the RolePermissions. Thus to find out whether the user is assigned to a certain role, the UserRolePermissions of a node which also has RolePermissions for that specific role must be checked.
Server Trace: The server can nr started with the trace levels info and debug and for clarity limited to the session facility, for the demoserver this is possible with the following parameters:
uaserverhp -d info,debug -f session
When a client connects or changes its user, the trace now has detailed information for each role and based on which criteria it is given to the user or denied to the user.
Previous versions of the SDK included the inode authorization backend which had a user-group-other concept for permissions, similar to the Unix-like permission model. This backend was replaced by the more powerful rolepermission backend, however for new or existing applications that prefer or already have the user-group-other model it is still possible to map that model to rolepermissions. This section shows how this mapping can be conceptually done.
With the rolepermissions backend only roles are possible, so the user-group-other permissions are replaced by role-role-role permissions with the following mapping:
When giving each node exactly these three roles it is also possible to reduce the cmake option UASERVER_MAX_ROLES_PER_NODE to 3 which can save some amount of memory.