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 (see PLC access must be restrict... (REQ_SEC_PLC_ACCESS)):

{
  "plc_access": ["Line1", "TestRig"]
}
  • If plc_access is present, the user can only access listed PLCs

  • If plc_access is 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 403 with PLC_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

Breaking change. Before this, an absent 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.