Authorization Model
Effective permission = role permissions ∩ token scopes
An operator with an ads:read-only token can only read.
Roles
| Role | Permissions |
|---|---|
viewer | Read variables, browse symbols, device info, read files |
operator | + Write variables, upload files |
admin | + Start/stop/reset PLC |
Scopes
| Scope | Grants |
|---|---|
ads:read | All read operations |
ads:write | Write operations (variables, files) |
ads:lifecycle | PLC lifecycle control |
Policies
| Policy | Requirements |
|---|---|
ReadAccess | plc_access check + role >= viewer + scope ads:read |
WriteAccess | plc_access check + role >= operator + scope ads:write |
LifecycleAccess | plc_access check + role = admin + scope ads:lifecycle |
Per-PLC Access Control
Add a plc_access claim to the JWT with an array of allowed PLC aliases:
{
"plc_access": ["Line1", "TestRig"]
}- If
plc_accessis present, the user can only access listed PLCs - If
plc_accessis absent, the user can access no PLC (default deny) plc_access: ["*"]reaches every PLC — how a service account that genuinely needs the whole fleet says so- Accessing a denied PLC returns
403withPLC_ACCESS_DENIED
A token the IdP never restricted is not thereby a token authorised for everything. Read the other way round: without default-deny, a compromised token that happens to carry no claim holds unrestricted access to every configured PLC.
The rule is the same everywhere the claim is read — per-PLC routes, the fleet-wide alarm routes, and the MCP tools and resources.
A claim that is present but unreadable (malformed JSON, null) always denies, whatever the
default is. The default governs absence only: an unreadable claim means the IdP meant to
restrict the token and adsify cannot tell how.
Migrating a deployment whose IdP does not issue the claim
plc_access claim granted access to every PLC.
Tokens without the claim now receive 403 PLC_ACCESS_DENIED.Add a plc_access claim to your tokens — see Keycloak for a
protocol mapper. Until that is in place, a deployment can restore the previous behaviour:
{
"Authorization": {
"PlcAccessDefault": "allow"
}
}| Value | An absent plc_access claim means |
|---|---|
deny (default) | The caller reaches no PLC |
allow | The caller reaches every configured PLC |
allow logs a warning at every startup. It is a migration aid, not a supported end state.