Roles
Roles define what a signed-in account can reach. Each role holds a list of permission rules covering three things — which UIs the account can open, which pages it can navigate to, and which API functions it can call. Users can hold several roles, and permissions add up across them.
Open Roles
Before you start
Editing roles is itself an administrative action. You must be signed in with a username and password on an account whose role has Elevated turned on. A session opened with a PIN cannot reach the admin console at all, and a non-elevated session is refused when it tries to write the role table.
If this controller is bound to a tenant's primary site through Account Sync, roles arrive from the primary and the page shows a yellow banner reading Roles are managed by the auth server. The grid drops to a single Reload button and the editor is hidden. Make role changes on the primary site instead; they replicate from there.
Viewing roles
The grid lists every role with an ID above zero and shows four columns:
| Column | Meaning |
|---|---|
| ID | The role's number. Used internally when a user's assignment is stored by ID. |
| Name | The role name — lowercase letters, numbers and underscores. |
| Rules | The compiled rule list. Read-only here; edit it in the role editor. |
| Enabled | Whether the role is active. |
Toolbar buttons are Add, Edit, Delete and Reload. Each row also carries a copy icon — Duplicate role — which loads that role's whole permission set into the editor as a new role named <name>_copy. Rename it, adjust, and save. That is by far the fastest way to make a near-identical role without re-ticking dozens of boxes.
Nothing stops you deleting a role that users still hold — including the role your own account uses. Reassign the affected users first (see Users), then delete.
Roles that ship with a new system
A fresh install seeds exactly two roles. Everything else is built by you.
admin — allow ui *, allow route *, allow api *, Elevated: on. This is the commissioning and administration role, assigned to the administrator account the installer created.
ui — allow ui *, allow route *, deny route /admin*, allow api *, Elevated: off. This is the wall-panel operator role. It is assigned to the built-in ui account, which signs in with a randomly generated 6-digit PIN and holds a 30-day session — long enough for a panel that nobody logs into for months, and short enough that it eventually expires.
Any role named admin is automatically switched to Elevated at startup, so an older system that predates the Elevated switch keeps behaving the way it always did.
On the two seeded roles the Allow Remote value was never written — the server reads that as "no remote restriction". The editor's switch has no way to show "never set", so it renders as off. Press Update Role and the off position is written for real, and the role becomes local-only from that moment — for every account holding it, including accounts whose other roles permit remote access, since one local-only role restricts the whole account. If you only opened the role to look at it, press Cancel. If you did save it, set Allow Remote to on deliberately and save again.
Creating a role
-
Click Add. The editor opens with Status on, Allow Remote off, Elevated off, Skip 2FA on Local Network off, and no rules.
-
Optionally click a starter template under Start From a Template (new roles only — the buttons are hidden when editing an existing role):
- Administrator —
allow ui *,allow route *,allow api *, Elevated on, Allow Remote on. Suggested nameadministrator. - Standard User —
allow ui *,allow route *,deny route /admin*,allow api *, Elevated off, Allow Remote on. Suggested namestandard_user. The typical resident or employee. - Local Panel — the same rules as Standard User but Allow Remote off. Suggested name
local_panel. Intended for wall panels and shared devices; narrow the allowed UIs to that panel's UI after applying.
A template fills the whole form. If you already typed a name it is kept; otherwise the suggested name is filled in. Adjust anything before saving.
- Administrator —
-
Fill in Basic Information (below).
-
Set the permissions under Permission Configuration.
-
Click Create Role (the button reads Update Role when you are editing an existing one). Cancel discards everything.
A role saved with no rules grants nothing at all — anything not explicitly allowed is denied.
Basic Information
Role Name — lowercase letters, numbers and underscores only. The field reformats itself when you click out of it, and a name with anything else in it is rejected on save with "Name must be lower case letters, numbers and underscores only".
The field stays editable after creation, but user accounts remember their roles by name. Rename a role and every user holding it silently loses those permissions — the server logs an invalid-role warning and the account is left with whatever its other roles grant. If you must rename, reopen each affected user in Users and re-tick the role afterwards.
Allow Remote — on permits sign-in from outside the building; off restricts the role to on-premises connections. The hint under the switch reads Remote access allowed or Local access only. When it is off:
- A login attempt from off-site is refused at the login screen with an explanation.
- A signed-in session that becomes remote is bounced back to the login screen with the same explanation, and the refused requests appear in Request History with the reason
remote_disallowed. - Camera streams are covered too. Live video runs over its own connection, which is checked with the same test when it is opened. Previously a local-only role that had signed in on site could keep pulling camera streams from anywhere; it is now refused like every other request.
- The REST API is covered too.
/api/tokenrefuses to mint a token for a local-only role when the request is provably remote, and every subsequent API request re-runs the test — a token minted on the LAN stops working the moment the same holder is off-site, and revoking Allow Remote mid-session takes effect on the holder's next request. When the server cannot tell where a request came from, nothing is denied.
Across several roles, off wins. Allow Remote is a restriction rather than a capability, so it is ANDed the way Skip 2FA on Local Network is: a user holding one local-only role is local-only, whichever other roles they hold and whatever order those roles sit in on the account. Adding a restricted role to a user always tightens their access. A role whose Allow Remote was never written at all counts as not restricted, so an account whose roles have never expressed an opinion keeps signing in from anywhere, exactly as before.
Remoteness is decided from two independent signals, either of which denies:
- The connection's own peer address, judged server-side. Loopback,
10/8,192.168/16,172.16/12, IPv6 unique-local and link-local, and the100.64/10CGNAT range that overlay VPNs such as NetBird and Tailscale hand out all count as on-premises — a user on the site VPN is treated as on-site. A public address is remote. Any connection arriving through the cloud tunnel is remote by definition. - The legacy client-reported WAN IP, compared against the server's own WAN IP.
Earlier versions used only the second signal, so a client that simply never reported a WAN IP was never checked. The peer-address test closes that: a client can no longer stay silent to avoid the check. Nothing that was blocked before is newly allowed, and when neither signal can reach a verdict the connection is treated as it was previously.
When GEM sits behind nginx or another reverse proxy, every connection's peer address is the proxy's, so the x-forwarded-for header carries the real client. That header is client-supplied and is ignored by default — honoring it blindly would hand the decision back to the party it constrains. Opt in under System → Server → Network → Reverse Proxy, or set trusted_proxies in gem.json directly:
{ "trusted_proxies": true }
true trusts any loopback peer (the single-box nginx case). To name specific proxies, use a list of the addresses they connect from:
{ "trusted_proxies": ["10.1.1.5", "10.1.1.6"] }
A peer that is not on the list is judged on its own address, not on what it claims. Leave the setting out entirely if you don't front GEM with a proxy — panels on the LAN are identified correctly without it.
When a trusted proxy passes a chain of addresses, GEM reads it right to left: each hop appends the peer it saw, so the last entry that is not itself a configured proxy is the caller, and the leftmost entry is simply whatever the client typed. The same value is used for the remote/local verdict, for the remote address lookup, and for the address recorded in Request History, so those three can never disagree about who the caller was. Connections arriving through the cloud tunnel carry no honored x-forwarded-for at all — the tunnel is not a proxy you configured.
Skip 2FA on Local Network — off (the default, and the value every role created before this switch existed carries) requires the emailed two-factor code on every sign-in. On waives it for members signing in from the local network, while sign-ins through the cloud connector still require it.
This exists because two-factor on the LAN is right for a bank branch and wrong for a family home. Most residences want the code when someone reaches the house from the internet and don't want it when a resident opens the app on the couch.
Three limits are worth knowing before you turn it on:
- It can only skip a code, never require one. Two-factor is enabled per account in Users. If an account has it switched off, that account is never challenged anywhere — this switch changes nothing for them, including through the cloud connector.
- Every role a user holds must permit it. Unlike Elevated, which is ORed together across roles, this one is ANDed — the same way Allow Remote is. A user holding both
resident(on) andintegrator(off) is challenged. Adding a strict role to a user always tightens their sign-in, never loosens it. - Only the local network qualifies, judged server-side. The waiver applies when the connection's real peer address is on-premises, using the same address test described under Allow Remote above — including the
100.64/10overlay-VPN range, so a user on the site VPN counts as local. Anything arriving through the cloud connector, and anything the server cannot positively place on the LAN, gets the code.
On an Elevated role, waiving the second factor means anyone on the local network who has the password alone can open a session that reconfigures the building. A guest on the Wi-Fi, a compromised laptop, or a device on an untrusted VLAN is on "the local network" for this purpose. The editor warns when the switch is combined with Elevated. Use it on resident and staff roles; keep the commissioning role on two-factor.
Sign-ins that skipped the code are logged as such on the server, so you can confirm the setting is behaving the way you expect.
When someone with this setting does get challenged — because they are away from the building — the login screen says so rather than presenting a bare code prompt. Without that, the same person on the same phone is asked for a code or not depending on which network they are on, which generates support calls. Users whose role always requires a code see the standard wording, since telling them where they are signing in from would only confuse.
Status — on enables the role, off disables it. A disabled role stops contributing permissions once the system has reloaded it. See Turning a role off for what that actually takes.
Elevated — grants administrative authority. Covered in detail under Elevated.
Current Rules
Below Basic Information, the Current Rules panel shows the rule list the form is currently producing, in a dark monospace box, one rule per line. It updates as you tick boxes, and it reads No rules configured yet when the role is empty.
Treat this panel as the authority on what the role will save. The checklists cannot always represent a rule visually — a wildcard grant in particular shows here as allow ui * with nothing ticked in the list.
Permission Configuration
Resource Type selects which of the three permission sets you are editing:
- User Interfaces — which UIs the role can open.
- Routes/Pages — which pages the role can navigate to.
- API Functions — which backend functions the role can call.
Under Resource Permissions each type shows two lists side by side — the allowed list on the left and the denied list on the right:
| Resource Type | Left list | Right list |
|---|---|---|
| User Interfaces | Allowed UIs | Denied UIs |
| Routes/Pages | Allowed Routes | Denied Routes |
| API Functions | Allowed API Calls | Denied API Calls |
The search box at the top right of the section filters whichever type is on screen — its placeholder changes to Filter UIs…, Filter routes… or Filter API calls… — and the × inside it clears the filter.
Every denied list has a Deselect All button in its header that unticks the whole list at once, which is far faster than clearing a long deny list box by box.
User Interfaces
The list contains the UIs configured on this system, by name. There is no "all" entry here — to grant every UI you tick each one.
The denied UI list only matches specific UIs. If you somehow end up with a deny ui * rule it has no effect at all — it will not block anything. To keep a role out of a UI, tick that UI in Denied UIs; to keep a role out of every UI, simply grant it none.
Because the shipped roles use a wildcard UI grant, opening admin or ui in the editor shows an empty Allowed UIs list even though every UI is allowed. The allow ui * line in Current Rules is what is really in force.
Ticking a UI in Denied UIs alongside a wildcard grant is the supported way to carve one UI out of a blanket allow, and the carve-out is honoured for commands as well as for opening the UI — see UI scope for commands.
Routes/Pages
The list contains every page in the product plus four wildcards you will use constantly: /*, /admin*, /controls*, /av*. An all entry sits at the top.
Real page paths look like /admin/devices, /controls/lights, /av/directv, /uis, /home. The usual pattern for a non-admin role is to allow everything and deny the admin section:
allow route *
deny route /admin*
Admin pages a role cannot reach are removed from the admin menu, and a menu section whose pages are all denied disappears entirely — so a Standard User who somehow lands on /admin sees nothing they cannot use.
API Functions
The list holds every backend function the system exposes, with an all entry at the top. It is grouped by area — auth, control, data, av, alarms, monitoring, backup, reports, users and access, and so on — so related functions sit together instead of forming one flat alphabetical wall. Group headings are hidden while a search filter is active.
In the Allowed API Calls list, functions that require administrative authority carry an elevated badge. There are over two hundred of them; see Admin-only operations.
Most roles should allow * here and rely on two other mechanisms to hold the line, exactly as the Standard User template does:
- Functions with the elevated badge are refused anyway unless the session is elevated.
- Anything you want blocked outright goes in Denied API Calls, which beats any allow.
Naming individual functions in the allowed list is for narrow integration accounts. Do it only when you can enumerate everything the account needs — a missing function shows up as an unexplained failure in the client, not as a helpful error.
Rule syntax
Each rule is one line:
<action> <type> <resources>
- Action —
allowordeny. - Type —
ui,routeorapi. - Resources — a comma-separated list, or
*for everything.
UI rules carry the numeric IDs shown in the grid's ID column; route and API rules carry the path or the function name:
allow ui 1,2,3
deny ui 4
allow route /controls*,/av*
allow api command,macro,zones
Ticking the all entry produces *. Every other tick contributes its own name.
Tick all and a few individual entries and the saved rule reads allow api all,command — a literal all that matches no function, plus the one function you also ticked. The wildcard is only produced when all is the only thing ticked. If you want everything, tick all by itself; if you want exceptions, allow * and put the exceptions in the denied list.
A rule that is missing a part, or whose action is neither allow nor deny, is skipped at load time with a warning in the server log naming the role and the offending line. The rest of that role's rules — and every other role — still load. Because a role with no rules grants nothing, check the log after hand-editing rules if a role suddenly has less access than expected.
How permissions are decided
- Deny wins. If any rule from any of the user's roles denies the resource, it is denied — a deny beats a wildcard allow and beats a by-name allow.
- Then allow. If any rule allows it, access is granted. Allows from multiple roles accumulate.
- Otherwise, denied. Nothing is reachable without an explicit allow.
Routes are matched as patterns, so /admin* covers every page beneath /admin. UIs and API functions are matched by exact ID or exact name — the only wildcard there is * itself.
Elevation and remote access are not rules; they are switches, and they combine across roles in opposite directions:
- Elevated is ORed. A user who holds one elevated role is elevated.
- Allow Remote and Skip 2FA on Local Network are ANDed. One role with Allow Remote off makes the whole account local-only, even if another role permits remote. One role with the 2FA waiver off means the code is always required.
The difference is deliberate: Elevated grants a capability, where the most permissive role should win, while the other two remove a protection — and there the most restrictive role must win, so that giving someone an additional role can never weaken their sign-in.
Worked example. A user holds panel_user and installer:
panel_user:
allow ui 3
allow api command,macro,zones
installer:
allow ui *
allow route *
allow api *
deny api backup_delete
Elevated: on
The result: every UI and page, every API function except backup_delete, and an elevated session — because installer contributes elevation and its deny survives the other role's allows.
Saving changes and making them take effect
Saving a role writes it to the database but does not immediately change what live sessions can do. Role and user edits are queued as pending changes.
- Save the role. A badge appears in the admin top bar with a count of pending changes.
- Click the badge to open the pending-changes panel. It lists the changes grouped by type.
- Click Apply All. The affected roles are reloaded and every user holding them is re-merged, and connected clients pick up the new permissions without reconnecting or signing in again.
Clear All — and the per-type Clear button — throw the queued reload away. The database still holds your edit, but the running system keeps using the old permissions until the next apply or restart. Closing the panel with ✕ leaves the changes pending.
Turning a role off
Switching Status off and applying drops the role from the server's cache and re-merges every user holding it, so its permissions stop applying at that point. No restart is needed.
It is a permission change, not a session end. Holders stay signed in with whatever their remaining roles grant: each live session is refreshed in place, and every request from then on is judged against the reduced permissions. To end the access outright rather than reduce it:
- Remove the role from the user in Users, then apply the change; or
- Disable the user, which ends their session and forces a fresh login.
Note also that a disabled role still appears in the role list on the Users page and can still be ticked there — it simply grants nothing once it has stopped loading.
Elevated
Elevated is the difference between a role that can run the building and a role that can reconfigure it. When it is on:
- Admin-only functions are reachable through a wildcard (
*) API grant. - The role can send commands to devices and zones marked Elevated and run macros marked Elevated.
- Secure attribute values (passwords, API keys, tokens) are readable.
- The role can write to any table through the generic create/update/delete/sort paths.
- Raw-query and attribute-write functions (
query_json,set_attribute) are available.
When it is off — the default, and the right answer for almost every role:
- Admin-only functions must be granted by name. A wildcard
*alone is not enough. - Commands and macros flagged Elevated are rejected.
- Secure attributes are filtered out of results, and sensitively-named values are scrubbed even if a row was flagged wrong.
- Generic writes are limited to
channel— everything else returnsnot authorized. Panel-side scene building doesn't need broader access: the lights, shades and AV scene editors call purpose-built scene functions that construct themacro,macro_stepandui_macrorows server-side (see themacro_stepbullet below).macroandui_macrowere on this list until scene creation and rename moved onto that path; nothing non-admin writes either table generically any more. - A row written through those paths may not take a name that is only a coercion artifact —
undefined,null,true,false,NaN,__proto__,constructor,prototype,[object Object], in any casing. They are refused withinvalid name. Macros are resolved by name from macro steps, schedules and remote key bindings, and an internal dispatch that names its macro by id carries no name at all, so a row stored under one of those literals could answer a lookup meant for a different macro — including the duress macro fired by a PIN entered under coercion. Ordinary names are unaffected, as are names that merely contain one of those words (undefined_scene,nullify). - A macro may additionally not take a name that is only digits (
7,42) — refused with "a macro name cannot be only numbers". Macros are the one entity resolved from both a name map and an id map, with several lookups trying the name first, so a macro named7answers a reference that meant macro id 7. The one that matters is a remote key binding, which is integrator config dispatched with the remote's own authority: bind a key to macro 7, let a non-elevated session store a light scene named7, and the button runs the scene elevated. Numeric references now resolve by id everywhere, and the string spelling can no longer be created. This rule applies at every elevation, integrators included — the ambiguity belongs to the name, not to whoever typed it. It is macro-only: a digit-only channel or zone name is ordinary, since nothing resolves those by name-or-id. Names that merely contain digits (scene_7,7am_wake) are fine, and no existing row is touched — a site that already has a macro named7keeps resolving it under that exact name. ui_macro— the row that puts a macro on a panel — is not writable through the generic paths either. The scene functions write the link server-side, scoped to the calling UI, and a link may not point at a macro flagged Elevated — judged on the stored macro as well as the incoming one, so re-labelling an existing button is covered. Without that, a kiosk session could add (or silently re-point) a tile that the panel's owner then taps with their authority. Placing an arbitrary macro on a UI is admin work, done in UI Macros.macro_stepis not on that list. A step's settings are free-form JSON on a row that later executes, which makes it the widest write a non-elevated session could hold, so authoring steps through the generic create/update paths is refused outright. Panel-side scene building is unaffected because it never used that path: the lights and shades scene editors call purpose-built functions that build each step server-side with a fixed target and level. Those scene functions carry their own gate for a non-elevated session: a step cannot be written into (or removed from) a macro flagged Elevated, cannot name a target the session could not command live (an elevated zone or device, or a zone on no UI the author can reach — see UI scope for commands), and cannot pull a zone from outside the scene's own subsystem. A scene save that lands on an existing macro — the panel-side editors locate their macro by name — adopts it only when the caller could have authored what is already there: every existing step must be a plain scene-style command step over targets inside the caller's own scope, so a non-elevated session cannot blank or repurpose an admin's macro by saving a scene under its name, and deleting a scene takes the same test. A scene may also not take a name that already belongs to a macro in another subsystem: macro names are resolved by name in macro steps, schedules and remote key bindings, so a duplicate would silently shadow the original — the nightly lock-up macro stops running and a lighting scene takes its place, with every row still looking correct. The stored level is coerced to a number on the way in, so a scene cannot carry framing characters into a driver template on a later run. The scene's name is normalized to identifier form (lowercase with underscores, the same rule every other macro name follows) before it is looked up or written, and a name left empty by that normalization is refused — a macro name is resolved by name from macro steps, schedules and remote key bindings, so a row whose name cannot round-trip as an identifier has no business being stored. The coercion literals above (undefined,null,__proto__, …) are refused here as well: normalization keeps them intact, and they are exactly the names an internal lookup can land on by accident. So is a digit-only name — a scene saved as7— which normalization also keeps intact and which would collide with the macro id space; that one is refused with "a scene name cannot be only numbers", since it is the spelling a person can reach by accident. Panel-written scene names are already in that form (they are built from the UI's name), so this changes nothing for an existing site; a pre-existing scene whose stored name the rule would now rewrite is still adopted rather than orphaned behind a duplicate. Creating a new scene and renaming one travel the same path: creation writes the macro shell and its UI link server-side, and a rename changes the macro's label and the calling UI's link name only — the internal name never changes, so schedules, triggers and remote key bindings keep resolving — after passing the same adoption gate, so a session cannot relabel a macro it could not have authored. The scene editors surface a refused write as "Scene change refused" (or "Rename refused") rather than reporting a save that didn't land.- Behind that table gate sits a second one, in force whenever a non-elevated session reaches the step handlers at all: the step's driver must be one of Command, Delay or Set Variable. It is written as an allowlist, so a step type nobody has reviewed — including one a future release adds — is refused until it is deliberately allowed, and a row naming no driver at all is refused rather than waved through.
- Command steps are additionally checked per target: a step aimed at a device or macro flagged Elevated is refused, exactly as the live command would be. This applies when re-pointing an existing step, not only when authoring a new one.
- A non-elevated session also cannot give a Command step a dynamic target — a
zone_id/device_idwritten as a placeholder such as[zone_id]. The token resolves to nothing while the step is being saved, so the per-target check above has nothing to judge; at run time it becomes whatever the caller passes in. Elevated authors may still target dynamically. - A Command step must also target a zone inside the author's own UI scope: when every UI the session's roles allow carries a zone scope, a step aimed at a zone on none of them is refused — you cannot persist a command you could not run. The scene editors apply the same check. Sites whose UIs express no scope are unaffected.
- The Elevated flag on a macro cannot be changed — a non-elevated session cannot clear the flag and then run the macro.
- Macros that already contain any of these steps keep running normally — the gate is on authoring, not execution.
- Test Condition (the If Attribute step's test button in the macro editor) requires elevation. Testing a condition runs it, and an if-step's payload can carry run-command / start-macro / set-attribute actions that would execute as a side effect. Those action keys are also stripped before the test runs, so a test never mutates anything.
- Test on the UI Command step requires elevation for the same reason: the test is the real broadcast, so it drives every panel on the targeted UIs — navigating them, reloading them, waking their screens. Its only caller is the macro editor.
- Enabling or disabling a security-bearing record is refused — user accounts, roles, access groups, access rules, access control, credentials, sites, clients, licenses, alarm definitions, notification profiles, web services and user drivers. Operational records like devices, zones, macros and monitors are unaffected, except a macro flagged Elevated: its Status switch is admin-only for the same reason its steps are. Turning off the nightly lock-up or the scheduled alarm arm would leave the schedule and trigger rows looking untouched.
- Generic reads of credential tables are refused and secure attribute values are filtered on both the admin interface and the REST API.
- Generic reads of
macroandmacro_stepare refused as well: a step'sdatais free-form JSON that can carry author-time secrets — Web Request auth headers, Run Script sources. The scene editors read through purpose-built functions instead, which return only the zone/level projection of a scene's command steps (and, for AV presets, an id/name/label list) — a macro that is not a scene exposes nothing through them. - A redacted column may not be used as a filter either. A query whose where clause names
pin,hash,salt,token,password— or any string column ending in_token,_secret,_passwordor_hash— is refused withinvalid query, whether it is named directly or through_like,_in,_gt,_between,_contains,_notequalor_or. Redaction only blanks the value on the way out; left filterable, the same column reads back a character at a time from a row count. Boolean flags with matching names (must_change_password) are unaffected. - Deletes, attribute writes and raw queries are refused outright.
Scoped administrators — full power with pull-backs
Elevation composes with deny rules, and deny wins over a wildcard allow. That means you can build a role the other way around from the usual grant-by-grant kiosk role: start from full administrative power and pull back the areas the role must not touch. This is the right shape for a department manager — facilities, landscaping, AV — who should run and reconfigure their part of the building without holding the keys to everything.
facilities_manager:
Elevated: on
allow ui *
allow route *
allow api *
deny route /admin/users*,/admin/roles*,/admin/visitors*,/admin/backup*,/admin/script_console*
deny api upsert_user,issue_visitor_pin,revoke_visitor
deny api backup_create,backup_restore,backup_restore_database,backup_delete,backup_download
deny api backup_dr_fetch,backup_dr_push,backup_settings,backup_save_settings
deny api download_encryption_keys,upload_encryption_keys,query_json
Keep the comma lists free of spaces: a rule line is split on spaces, so anything after a space in the resource list is silently ignored. When pulling back an area, enumerate its functions from the Allowed API Calls checklist in the role editor — the list groups related functions together, which is the reliable way to catch every member of a family like backup.
This role reaches the whole admin console except user management, backups and the script console. Everything else — devices, zones, macros, schedules, monitoring — carries full administrative authority, including admin-only functions, which the wildcard grants because the role is elevated. As always, the session must be opened with a username and password: a PIN never carries the admin surface, regardless of role (see the warning below).
Two things to keep in mind when building one:
- Deny the API functions, not just the routes. Hiding
/admin/backupremoves the page; denyingbackup_downloadrefuses the operation itself, including for a client that calls the socket API directly. - Pull-backs are guardrails, not containment. An elevated session can write any table through the generic record paths, so a determined administrator could still edit users or roles even with the pages and named functions denied. Elevation is a statement of trust in the person — use pull-backs to keep an honest manager out of areas that aren't theirs, not to contain someone you don't trust with elevation.
A role grants elevation. The credential you used decides whether the session actually carries it. Signing in with a PIN never grants admin authority, even for a user whose role is elevated.
A PIN is a door credential — 4–8 digits, typed on wall keypads in front of whoever is standing there, shared with staff, and mailed to visitors in plaintext. Treating it as equivalent to a password would make it an admin password.
A PIN session keeps:
- Access to its UIs, controls, scenes and panel-side scene building
- Commands to devices flagged Elevated and macros flagged Elevated — a PIN already opens those at the keypad, so a panel that PINs in to unlock a door keeps working
- Site-mode changes, when the role is elevated — setting Away or Home from a wall panel is what panels are for
A PIN session loses, regardless of role:
- Every admin-only function (the elevated badge in the API checklist)
- Secure attribute values, attribute writes, raw queries, deletes, and generic writes outside the allowlist
- Backup download and the privileged macro-step types
- The
/adminconsole — requesting it bounces to the login screen with a prompt to sign in by password
To do admin work, sign in with a username and password. Denials of this kind are recorded in Request History with the reason admin_only_pin_session, so an admin who expected access can see why it was refused.
Elevated devices and macros
Separately from role elevation, individual devices, zones and macros can be flagged Elevated. Only a session that carries elevation can:
- Send a command to an elevated device — checked both on direct device commands and on zone commands that resolve to an elevated device
- Send a command to a zone flagged Elevated itself — the per-zone switch for hardware hosting zones of mixed sensitivity (an intercom that is also a door strike, a relay board driving both lighting and a gate), where flagging the whole device would lock ordinary users out of the benign zones too. Either flag refuses independently; see Zones
- Run an elevated macro, whether from the app, a command-form macro redirect, or a voice-assistant request
- Edit an elevated macro, change the Elevated flag itself, or switch the macro's Status on or off
The gate applies to every client transport. The admin interface, the REST API (POST /api/control/command and /api/control/macro), the Ask GEM voice assistant and remote key payloads resolve the target the same way and return the same not authorized refusal, so a device or macro that is admin-only on one is admin-only on all of them. A command that names several zones at once is judged on every zone it can resolve — the id list and any zone name in the same payload — so a mostly-harmless batch cannot carry one elevated target through.
A remote key payload that names its own macro, zone or device by id is stricter still: it is never treated as elevated, on any transport, including a physical remote wired to the TCP/serial listener or reaching GEM through the Remote Proxy driver. Those transports carry no credential — the address identifies the connection, not a person — so the id the caller supplied is judged on its own. Binding the macro or command to a key on the remote's row is the supported path: a key binding is the integrator's configuration and carries the remote's authority, the way a wall keypad does.
A related confinement applies to raw hardware addresses. Some commands accept an address argument naming a specific output on the device — a relay port, a PDU outlet, a breaker. For a non-elevated session, a supplied address must be one the system would have resolved on its own: an output published as a zone on the target device (provided that zone is not itself flagged Elevated), the resolved zone's own address, or the device's address. Anything else — including the outputs an integrator deliberately left unmapped, which are usually the sensitive ones — is refused with not authorized: address is not mapped to a zone on this device, and an address naming an elevated sibling zone is refused with not authorized: address is an elevated zone on this device — on hardware hosting zones of mixed sensitivity, the per-zone Elevated switch cannot be sidestepped by naming that zone's address in the arguments of a command aimed at a benign sibling. This is enforced centrally at the command dispatch layer, so it covers every driver and every command at once. Elevated sessions keep full access for commissioning and Commands-tab testing; internal callers (macros, triggers, schedules, drivers) are unaffected, as are addresses the server resolves or reads from stored configuration on the caller's behalf — AV volume routing, per-zone <verb>_action overrides, remote key bindings, device-combiner routing templates, and the literal arguments of admin-authored macro steps whose target is literal as well, which are integrator configuration rather than caller input. (A step that pairs a stored address with a [zone_id] target is not exempt: the admin chose the address, but the caller chose the device it lands on.) Drivers that accept the address under another argument name (relay, access_point_id) are covered under those spellings too.
Some drivers reach a fixed output through a named verb instead of an address argument — Big Dog Power's output_on / output_off drive the chassis relay with nothing in args to confine. Those verbs declare the address they imply and are judged against it, so the confinement can't be sidestepped by choosing the verb spelling over the address spelling. As with raw-protocol commands below, the verb is judged on the command row the payload resolves to as well as on the spelling the caller sent, so a payload naming its command by command_id and carrying no verb at all is judged on the row's own name rather than passing with nothing to check. An implied address is never treated as server-resolved.
A caller may not name a zone inside the arguments either. args.zone_id is the server's to fill: it is stamped from the zone the command actually resolved, and several multi-zone drivers (ONVIF, Sonos, Symetrix, Powersoft, Global Caché, Pentair ScreenLogic, Bogen Nyquist) read it to decide which zone of the device they are acting on. Both gates above judge the target named at the top level of the payload, so a zone named inside args is invisible to the elevation check and to the UI scope check alike — on a camera coordinator that would let a session whose UI carries one camera pull stills from every other camera the same device manages. For a non-elevated session the supplied value is therefore dropped before dispatch and replaced with the zone the server resolved (nothing at all on a device-direct command), and the drop is logged. Nothing in the product sends it — the app passes zone_id at the top level of the command, where both gates can see it — so this is invisible in normal use. Elevated sessions and internal callers keep whatever they set.
Three further confinements apply to a non-elevated session at the same central dispatch layer.
Raw-protocol commands — the commissioning escape hatches whose whole purpose is "send these exact bytes to the hardware" (passthrough, raw, raw_command, send_raw, send_serial, send_command, send_string, execute_raw) — are refused outright, because the payload is the protocol frame and carries no address or zone for any other gate to reason about. The refusal is judged on the command row the payload resolves to, not on the spelling the caller sent: an alias such as pass_through, and a payload that names its command by command_id and carries no name at all, are both refused on the row's own name.
Argument values may not carry framing. A value containing control characters (CR, LF, NUL, …) or a backslash-hex escape such as \0d is refused with not authorized: invalid characters in argument, since on the CR-delimited protocols many drivers speak such a value would smuggle a second, unchecked command onto the wire inside a permitted one. Values inside arrays and objects are checked too, not only plain strings — an array reaches the protocol template as its joined text. On a device whose command encoding is hex, a value containing whitespace is refused for the same reason: each space-delimited token there becomes a raw byte, so F1 0D 21 is clean-looking text that decodes to a second frame. No legitimate argument — a level, a channel, a source name, a setpoint — contains any of these.
The same test runs again on what the argument resolves to. An argument's […] token is resolved by the server against the command's device, zone and subsystem before the frame is rendered, and a value that names something which resolves to a control character is ordinary text when it is typed — so judging only the typed spelling let a caller name a framing character instead of writing one. Argument values are also substituted into the protocol template last, after the template's own attribute and variable tokens (see Commands → Substitution order), so an argument cannot introduce a token for a later pass to expand either. Elevated sessions and internal callers are unaffected by both.
Callers may not author the protocol frame. template is a field of a command row, not of a dispatch payload. It is dropped from a non-elevated caller's command before the driver renders anything, so a payload that names no raw command and touches no argument still can't put arbitrary bytes (or an arbitrary HTTP path) on the wire with a command row's authority.
Elevated sessions and internal callers (macros, triggers, schedules, drivers) are unaffected by all three, so commissioning still works.
Voice requests carry a tighter version of the UI scope below: every command the assistant sends is confined to the one panel it is speaking from — that UI's zones, and the devices those zones reach (including AV-source devices, where a source dedicated to another room's AV zone stays out of reach). A phrase that resolves outside the panel's scope is refused rather than dispatched, even when the same user could reach the target from another UI — a spoken command is contextual to where the person is standing, where a tapped button is not. A panel whose UI expresses no zone scope is unaffected. See Ask GEM.
Server-side triggers, schedules and driver-initiated runs are unaffected by the flag — the gate is on client entry, not internal execution. When a macro is started from a client, the caller's authority travels with the run, and each Command step is re-checked on the parts the caller influenced. A step whose target and command are exactly what its author stored runs as authored, even against an elevated device: authoring steps requires an elevated session, so a literal step naming a lock is a capability that author deliberately granted, and the step editor warns while it is being built. A step written with a placeholder such as [zone_id] takes its target from the caller's own arguments, so it is re-checked after substitution and refuses for a non-elevated caller when it resolves to an elevated target — a substituted command name on a literal device is judged the same way. The same principle covers the other admin-authored dispatch paths: a zone <verb>_action override, a remote key binding, and a device-combiner routing template each run their stored literal target without an elevation re-check, because no part of that payload came from the caller. Use this to expose scenes and routine controls to non-admin users while keeping security panels, locks and privileged macros admin-only. See Devices and Macros for per-entity configuration.
UI scope for commands
Separately from elevation, a non-elevated user's commands are confined to the parts of the building their own UIs reach. When the socket API or the REST API dispatches a command for a non-elevated session, the target must be on a UI the user's roles allow:
- Zones — the union of every allowed UI's zones: the zones of its linked Site Space when it has one (descendants included when Show Child Spaces is on), unioned with its own UI Zone rows. A space-linked UI's overlay rows count too, so a zone added to a room panel by hand — a room's AV zones when they aren't space-tagged — is in scope exactly as a space zone is.
- Devices — the devices backing those zones, plus AV-source devices reachable from an in-scope AV zone. A global (matrix-routable) source counts when any AV zone is in scope; a source dedicated to a single AV zone counts only when that zone is.
A command outside that scope is refused with not authorized before it reaches the dispatcher — over REST that is a 403, logged in Request History with reason scope_denied.
The gate arms only where the admin scoped every UI the user can reach, and fails open for compatibility everywhere else:
- A role with a wildcard UI grant (
allow ui *) and nothing in its denied UI list is unrestricted. - A wildcard grant with a carve-out (
allow ui *plus one or more Denied UIs) is not unrestricted. It is scoped to the UIs the role may actually open, so a zone that lives only on the carved-out UI cannot be commanded either — the deny binds commands exactly as it already binds page access. - An allowed UI that resolves to zero zones — no linked site space, no UI Zone rows — is an unscoped UI and leaves the user unrestricted, so sites that never expressed UI scope behave exactly as before.
- Elevated users are never scoped, and neither are internal callers — macros, triggers, schedules and drivers.
- Macro runs are not scoped by this gate as a whole. Macros are site-level artifacts with their own gates: the Elevated flag and the per-step checks above. A Command step whose target came from the caller's own arguments is the exception — a
[zone_id]the caller filled in is caller input, not the author's configuration, so it is checked against the caller's UI scope after substitution and refused withnot authorizedwhen it lands outside. Steps that run exactly the target their author stored are unaffected, as are triggers, schedules and driver-initiated runs, which carry no caller.
The same rule applies at authoring time: a non-elevated session cannot save a scene step or a macro Command step aimed at a zone outside its scope — you cannot persist a command you could not run. Voice is scoped more narrowly still, to the single panel spoken at (see Elevated devices and macros).
Admin-only operations
Over two hundred functions are marked admin-only and carry the elevated badge in the API checklist. They require an elevated session or an explicit by-name grant — a wildcard * allow is not sufficient, which is what prevents accidental exposure when broad permissions are assigned. Among them:
- System control — restart, update, and the script-execution functions
- Backup and restore — creating, restoring, deleting and downloading backups, and every SFTP, S3 and SMB variant of those
- Encryption keys — downloading and uploading the key material
- Database maintenance — maintenance runs, data-retention cleanup, and applying or clearing pending changes
- SSL/TLS — saving the certificate configuration and regenerating certificates
- User management — creating and updating accounts, issuing and revoking visitor PINs
- Monitoring — the whole Monitoring surface, including creating, editing and testing monitors and monitor tags
- Alarms — alarm history and statistics, shelving and unshelving
- Reports — dashboards, activity, exports, definitions and schedules
- Network scanning and discovery — ARP scan, the full network scan, driver ranking, adding a device from a discovery result, interface listing, ONVIF scan and probe
- Import / Export — artifact export, import preview and import
- AI assistant — every assistant function, including provider configuration and undo
- Integrations — LoRaWAN, Matter, MQTT and web-service traffic subscriptions, IR code rendering for Command Sets, and channel sync
- Logs and clients — tailing and searching the system log, and identifying connected clients
- System and support information — the system-information probe, sending a support bundle, license activation, the support contact record, tunnel status, and the pending-changes delta
- Configuration inspection — evaluating an automation expression against live entities, and looking up what references a record
The list is maintained as an allowlist of what is admin-only, so a function that is not on it is
reachable by any signed-in role a wildcard * grant covers. Several functions above were added to
it after review found them reachable by the built-in ui wall-panel role: the system-information
probe returns running-process command lines (which carry camera stream credentials), the discovery
functions map every camera, door controller and alarm panel on the network, and expression
evaluation resolves live entities including their secure attributes.
Ordinary control paths are deliberately not on this list — sending a command, running a macro, proxying an image or JSON payload, and voice-assistant requests stay reachable by non-elevated roles because end-user control surfaces and widgets depend on them. Reachable is not the same as unrestricted: each of those still resolves its target and refuses an elevated device or macro (see Elevated devices and macros).
Site-mode changes (set_site_mode, clear_site_mode, set_site_mode_hold) sit in a softer tier of their own. They carry the same elevated badge in the API checklist, but the requirement is an elevated role rather than an elevated session: a role with Elevated on reaches them through a wildcard * grant, and keeps them when signed in by PIN — changing the mode from a wall panel is ordinary daily control for the people who hold those roles. A non-elevated role still needs the explicit by-name grant, because a mode change runs the mode's whole transition action set, which on a typical configuration is the arm/disarm and lock surface.
The Ask GEM voice surface applies the same requirement at the point the skill is dispatched, so "we're home" from a guest tablet or a lobby PIN panel is refused exactly as set_site_mode would be over the socket. Asking what the mode is — status and list phrasings — stays open to any role.
The JSON proxy is restricted further for a non-elevated session, because it reaches the building network by design — that is what lets a widget fetch from a device on the LAN. For a non-elevated caller the request shape is reduced to a read:
- the method is forced to GET
- any request body is dropped
- request headers are filtered to
user-agent,acceptandaccept-language—Authorization,Cookie,X-Forwarded-Forand device-specific auth headers are removed
This keeps the weather, news and quote widgets working (all plain GETs) while removing the ability for a keypad-grade session to POST to an unauthenticated relay or unlock endpoint on the LAN. An elevated session still sends the full request unchanged, which is what admin tooling such as the IR database importer needs. The destination itself is not restricted by this — the proxy has always been allowed to reach private addresses.
Every admin-only request, granted or denied, is recorded in Request History, along with commands, macro runs, attribute writes, reloads, record writes, enable/disable actions and site-mode changes.
Functions a by-name grant cannot open
The by-name grant above is a deliberate hatch: it lets a narrow role — a kiosk, an integration account — be handed one admin-only function without being made elevated. That is right for narrow operations and wrong for a handful that are equivalent to root on the controller. Five functions are therefore refused unless the session itself is elevated, whether or not the role names them:
| Function | Why |
|---|---|
| Run server script / run client script | The Script Console executes arbitrary JavaScript with the live server instance in scope. There is no sandbox worth relying on here. |
| Update GEM | Replaces the application on disk and restarts the service. |
| Restart GEM | Takes the site offline. |
| Save server configuration | Rewrites the controller's configuration file. |
Because it is the session that must be elevated, a PIN session is refused as well — a wall panel whose role names these functions still cannot reach them. Granting one of these by name now has no effect; use an elevated role and a password login instead. The AI assistant's role-building skills strip admin-only names from an allow list by default, so a role built that way was never relying on this.
Two further admin-only functions were added to the list after review: the device web interface
lookups behind the Devices page's Web Interface tab. They
hand back an authenticated tunnel into a device's own configuration page, with GEM supplying the
stored web_username / web_password, so a non-elevated session that could name a device id reached
the admin interface of every HTTP device on the network. The tunnel each request opens is now valid
for the single device it was issued for.
Reloading records
Reload is not a refresh. It tears a record's live object down and rebuilds it — reloading a device disconnects its driver, so a repeated reload keeps a door controller, alarm panel or camera offline for as long as it continues. Other targets rebuild session rules, access rules, site modes and the server configuration.
Reload is therefore restricted to elevated sessions, with one exception: macros, which the
lighting and shade scene editors reload after saving a scene. Panel-side scene building is
unaffected; every other target returns not authorized for a non-elevated session. Use the relevant
admin page — Devices, Zones — to reload anything else.
Enabling and disabling records
Turning a record's Status off through the generic enable/disable function is treated as a write, and is judged against the same short list of tables a non-elevated session is allowed to write — rather than a list of tables it is forbidden to write. The practical effect is that a non-elevated session cannot switch off a trigger, a schedule, a monitor or a device. Those are the records that would report an intrusion, so being able to turn one off and back on again is close to the whole attack. Everything a non-admin surface legitimately toggles is unaffected.
The old /reload maintenance page, which sat outside /admin* and was the only non-admin caller of
that function, has been removed. Use Devices, Zones or
the relevant admin page to enable, disable or reload a record.
Role recipes
Homeowner / occupant. Use the Standard User template unchanged, then narrow the UIs if the site has panels that shouldn't be reachable from a phone.
Wall panel. Use the Local Panel template, then switch Resource Type to User Interfaces and tick only that panel's UI. Assign it to a dedicated account with a PIN rather than a personal account — see Users.
Commissioning / installer. Duplicate admin, then set Allow Remote off so the role only works on site. Leave Elevated on — commissioning needs it. Retire or disable the account when the job closes.
Read-only display. Allow the UIs the display shows, allow route /controls*, and in Denied API Calls tick command and macro. The screen renders live state but the touch surface does nothing.
Third-party integration. Grant no UIs and no routes; in Allowed API Calls tick only what the integration calls — commonly zones, attributes, command and macro. Verify each name against the checklist before saving; a function you forgot fails silently at the client.
Security practice
- Least privilege. Grant the minimum that lets the account do its job. Elevated is not a convenience switch.
- One role per purpose. A role per job type is easier to audit than one role with exceptions bolted on.
- Local-only where it fits. Installer and admin roles rarely need remote access; wall panels never do.
- Watch role combinations. Elevation ORs together across roles, so one permissive role can undo the restraint in another. Allow Remote goes the other way — one local-only role holds the whole account on site.
- Review periodically. Walk the role list and the user list together, and check Request History for denials that suggest a role is mis-scoped — or for grants that suggest one is too broad.
Functions worth keeping out of everyday roles even when the role is elevated: record deletion, backup deletion and restore, account creation and update, and restart. Put them in Denied API Calls on the roles that don't need them.
Troubleshooting
A user can't reach something they should
- Confirm the user has the role assigned, in Users.
- Confirm the role's Status is on.
- Open the role and read Current Rules — the checklists can hide a wildcard, that panel cannot.
- Check the denied lists on every role the user holds. One deny anywhere beats every allow.
- If the user is off-site, check Allow Remote on every role they hold — one local-only role keeps the whole account on site.
- Check whether you actually applied the change — an unapplied edit sits in the pending-changes panel doing nothing.
- Check whether they signed in with a PIN. A PIN session has no admin authority regardless of role.
A user can reach more than they should
- Open Current Rules on each of their roles and look for
*. - Remember that elevation ORs across roles — one elevated role elevates the whole session.
- Add the specific functions or pages to the denied lists rather than trying to enumerate every allow.
- Apply the change, and confirm in Request History that the calls are now refused.
"all" doesn't seem to work
all is the label on the checkbox; * is what gets saved. It only becomes * when all is the only thing ticked in that list — tick anything alongside it and the rule keeps a literal all that matches nothing. Untick everything else, tick all, and confirm Current Rules shows allow api *.
For UIs there is no all entry at all, and a deny ui * rule is inert. Grant or deny UIs one at a time.
Changes aren't taking effect
Role edits queue as pending changes. Open the badge in the admin top bar and click Apply All. If you clicked Clear All instead, reopen the role, save it again, and apply.
If a role was disabled and its users still have the access, that is expected — see Turning a role off.
Finding the exact name of a function or page
Edit any role, set Resource Type to API Functions or Routes/Pages, and type into the search box. The lists are generated from what the system actually exposes, so what you see there is the complete and current set — there is no separate list to consult.
Related documentation
- Users — creating accounts and assigning roles
- Account Sync — roles managed centrally across a multi-site tenant
- Visitors — time-boxed PIN credentials
- Sites — restricting an account to particular sites
- Access Activity — physical access events
- Request History — the API request log, with denial reasons