Access control
Fifty variables organised to exercise every combination of access-level flag, user role, and data type. The systematic test surface for access-attribute logic.
Path: TestServer / AccessControl
A test surface for the OPC UA access-level model. 50 variables split across 5 groups: access levels, AdminOnly, OperatorLevel, ViewerLevel, and AllCombinations.
1. Access levels
Path: AccessControl / AccessLevels
5 variables, one per access-level flavour:
| BrowseName | accessLevel | userAccessLevel | Initial | Notes |
|---|---|---|---|---|
CurrentRead_Only |
CurrentRead |
CurrentRead |
42 |
Write → Bad_NotWritable |
CurrentWrite_Only |
CurrentWrite |
CurrentWrite |
0 |
Read → Bad_NotReadable |
ReadWrite |
CurrentRead+Write |
CurrentRead+Write |
100 |
Full RW |
HistoryRead_Only |
CurrentRead+HistoryRead |
same | 200 |
History-read enabled |
FullAccess |
CurrentRead+Write+HistoryRead |
same | 300 |
Everything works |
CurrentWrite_Only uses only the CurrentWrite bit in
both accessLevel and userAccessLevel — the server cannot
read either. The "server-could-but-user-cannot" framing below
applies to the OPC UA model in general, not to this specific
node.
accessLevel vs userAccessLevel
The OPC UA spec gives each variable two access-level attributes:
accessLevel(id 17): server-level capabilities — what the variable can do.userAccessLevel(id 18): per-user capabilities — what the current session is allowed to do.
For CurrentWrite_Only, the server could read (the variable
holds a value internally), but the user cannot. accessLevel
includes CurrentRead; userAccessLevel is only CurrentWrite.
Reading id 17 vs id 18 separately is the standard test for "did your client expose both attributes correctly?".
2. AdminOnly
Path: AccessControl / AdminOnly
Variables intended for admin-only access. OPC-level access is RW for all; role enforcement is server-side.
| BrowseName | Type | Initial | Purpose |
|---|---|---|---|
SecretConfig |
String | "secret-value-123" |
Sensitive config |
SystemParameter |
Int32 | 9999 |
System-level parameter |
CalibrationFactor |
Double | 1.0 |
Calibration coefficient |
MaintenanceMode |
Boolean | false |
Maintenance flag |
Behaviour:
| Connected as | Read | Write |
|---|---|---|
admin |
OK | OK |
operator |
OK | OK |
viewer |
OK | OK |
| anonymous | OK | OK |
Despite the folder name, the AdminOnly nodes do not carry
role-protected write hooks in the current build — they're
plain RW variables. Role enforcement is only wired for the
variables under OperatorLevel/ (see below). Treat
AdminOnly/ as "tagged for future hardening" rather than an
authoritative gate.
3. OperatorLevel
Path: AccessControl / OperatorLevel
Variables with role-based write protection. admin and
operator can write; viewer cannot.
| BrowseName | Type | Initial | Purpose |
|---|---|---|---|
Setpoint |
Double | 50.0 |
Process setpoint |
MotorSpeed |
Int32 | 1500 |
Motor speed (RPM) |
ProcessEnabled |
Boolean | true |
Process enable flag |
RecipeName |
String | "Recipe_A" |
Active recipe name |
Behaviour matrix:
| Connected as | Read | Write |
|---|---|---|
admin |
OK | OK |
operator |
OK | OK |
viewer |
OK | Bad_UserAccessDenied |
This is the canonical test for "role-aware writes" in your client.
4. ViewerLevel
Path: AccessControl / ViewerLevel
Read-only by design. All five roles can read; none can write.
| BrowseName | Type | Value | Notes |
|---|---|---|---|
ProductionCount |
UInt32 | 12345 |
Static |
MachineName |
String | "Machine-001" |
Static |
IsRunning |
Boolean | true |
Static |
CurrentTemperature |
Double | 45.2 |
Static — no per-read noise |
UptimeSeconds |
UInt32 | 86400 |
Static — does not increment |
All five ViewerLevel variables hold static values set once at
server start. They are read-only via the access-level bits, but
none of them are updated by a timer or by a per-read hook — use
the variables under Dynamic/ if you need genuinely changing
data.
5. AllCombinations
Path: AccessControl / AllCombinations
Every combination of 8 data types × 4 access modes = 32 variables. Systematic test surface.
Access mode suffix legend
| Suffix | Access (accessLevel + userAccessLevel) |
|---|---|
_RO |
CurrentRead |
_RW |
CurrentRead + CurrentWrite |
_WO |
userAccessLevel = CurrentWrite (server can read) |
_HR |
CurrentRead + HistoryRead |
Type matrix
| Type | _RO value |
_RW initial |
_WO initial |
_HR initial |
|---|---|---|---|---|
| Boolean | true |
false |
false |
true |
| Int32 | 42 |
0 |
0 |
100 |
| UInt32 | 42 |
0 |
0 |
100 |
| Double | 3.14 |
0.0 |
0.0 |
2.71 |
| String | "readonly" |
"readwrite" |
"writeonly" |
"history" |
| DateTime | DateTime.UtcNow at startup |
same | same | same |
| Byte | 0xFF (255) |
0 |
0 |
0xAB (171) |
| Float | 1.5 |
0.0 |
0.0 |
2.5 |
Variable list
AllCombinations/Boolean_RO Boolean_RW Boolean_WO Boolean_HR
AllCombinations/Int32_RO Int32_RW Int32_WO Int32_HR
AllCombinations/UInt32_RO UInt32_RW UInt32_WO UInt32_HR
AllCombinations/Double_RO Double_RW Double_WO Double_HR
AllCombinations/String_RO String_RW String_WO String_HR
AllCombinations/DateTime_RO DateTime_RW DateTime_WO DateTime_HR
AllCombinations/Byte_RO Byte_RW Byte_WO Byte_HR
AllCombinations/Float_RO Float_RW Float_WO Float_HR
Test patterns
Read-only rejection
for type in [Boolean, Int32, ..., Float]:
status = write(AllCombinations/{type}_RO, sample_value)
assert status == Bad_NotWritable
Write-only rejection on read
for type:
dv = read(AllCombinations/{type}_WO)
assert dv.statusCode == Bad_NotReadable
Full RW happy path
for type:
write(AllCombinations/{type}_RW, sample_value)
assert read(AllCombinations/{type}_RW).value == sample_value
HistoryRead flag
for type:
access = read_attribute(AllCombinations/{type}_HR, AccessLevel)
assert (access & HistoryRead) != 0
Role-based write
Connect to opcua-userpass (4841) as viewer:
status = write(AccessControl/OperatorLevel/Setpoint, 60.0)
assert status == Bad_UserAccessDenied
Same write as operator → Good.
Where to read next
- User accounts and roles — the role-to-permission mapping.
- Browse paths and access levels — the access-level legend.