Skip to content

Security Model

Finnegan's Owner edited this page Jul 14, 2026 · 2 revisions

Security Model

RBAC replaces the engine's default always-allow authorization controller with a server-side controller that checks every REST operation against the caller's role. This page describes where the enforcement boundary actually is, what users see when they are denied, and the operational hazards you should plan for.

Enforcement Is Server-Side

The client plugin hides task buttons and menu items the user cannot use. That is a convenience, not the security boundary. Every operation is checked on the server when the REST call arrives, so a request sent directly with curl or a script is subject to exactly the same permission check as one sent from the Administrator. If the client fails to load the user's permissions at login, it fails closed and hides every task until a retry succeeds; the server keeps enforcing regardless.

Deny by Default

A user with no role assigned is denied everything beyond the infrastructure operations listed below. They can log in and change their own password, but channel lists come back empty and any permission-gated operation is refused. Access starts at zero and is granted, never assumed.

One deliberate exception: an operation with no permission mapping at all (for example, a third-party plugin endpoint declared without a permission) is allowed for any authenticated user with a role, and the server logs a warning naming the operation. Denying unknowns would break plugins the controller has never seen, which is a worse failure mode. Watch the server log for allowing unknown operation lines if you want to audit what falls through.

What a Denial Looks Like

A denied operation returns HTTP 403 with a reason phrase naming the missing permission, for example Missing permission: viewMessages. The Administrator surfaces that text in its error dialog, so a user (or the admin helping them) can see exactly which grant is missing instead of a generic "forbidden". Denied attempts are also recorded as failure events in the server event log.

Graceful Degradation

A handful of core engine reads (getChannels, getAllUsers, getUser, getChannelIdsAndNames, and similar) are designed by the engine to degrade rather than error: when the caller is not authorized, the servlet returns an empty list, or only the caller's own record, instead of failing. For these operations RBAC reports "not authorized" quietly instead of throwing a 403, so the engine's degradation works as designed. The practical effect: a limited user still logs in cleanly, sees an empty channel list rather than an error storm, and sees only their own entry in the user list.

Always-Allowed Operations

A small whitelist is open to every authenticated user because the client needs it to start up at all:

  • getStatus and getServerId - basic server status and identity checks.
  • RBAC's own getMyPermissions and getExtensionTaskPermissions - the client fetches these after login to know which tasks to show.

Separately, any user can update their own user record and password, regardless of role. Without this, the forced password change on first login would lock out anyone whose role lacks user management.

Admin Role and Lockout Guards

Users assigned to the role flagged is_admin bypass all permission checks. The bypass is by role flag, not user ID, so multiple admin users are supported. User 1 is only special to the startup seeder: a fresh install assigns it to the admin role, and any later restart re-assigns it if it somehow ended up with no role. Enforcement itself never looks at the user id.

Several server-side guards make it hard to lock yourself out:

  • The admin role cannot be deleted. Its name and description can change; the flag cannot.
  • The admin role must keep the full core permission set and cannot be given channel restrictions.
  • The last admin user cannot be unassigned or moved to a non-admin role. Assign a second admin first.
  • The last admin user cannot be deleted from the Users panel, even by an admin. The guard runs ahead of the admin bypass, so an admin cannot delete themselves out of the system.

The "last admin" count only includes live users. Assignments left behind by users deleted from the engine do not count toward the floor, so an orphaned row cannot mask a real lockout.

manageExtensions Is a Privileged Grant

Treat manageExtensions as admin-equivalent. A user holding it can disable the RBAC extension, and on the next restart the engine reverts to its default always-allow controller, making every user an effective admin. This mirrors stock OIE, where manageExtensions already means "install and run arbitrary plugin code". The role editor flags the permission with a warning tooltip. Grant it only to people you would trust with the admin role itself.

Operational Hazards

These are documented behaviors to plan around, not bugs.

RBAC can silently revert to allow-all in two ways. First, an engine upgrade: the plugin declares mirthVersion 4.6.0 and the engine matches it by exact equality, so after any engine version bump an un-rebuilt RBAC plugin is unloaded and the engine falls back to the default always-allow controller, with only a log line to tell you. After every engine upgrade, verify the RBAC settings tab is present and permission gating is active before letting users back in. Second, a manageExtensions holder disabling the extension, as described above. There is no in-UI recovery during the disable-to-restart window; re-enable the extension and restart.

Some in-panel edit controls are cosmetic-only on the client. The client hook governs task-pane items (Refresh, Save, Import, Export), not every button the engine builds into a settings panel body. Example: a user with viewConfigurationMap but not editConfigurationMap has Save and Import hidden on the Configuration Map tab, but the panel's own Add/Remove buttons and editable cells stay live. They can type into a row they can never persist. This is cosmetic, not a hole: the Save task stays hidden and the save operation is gated server-side regardless. The engine exposes no per-widget authorization hook, so these panel-body controls cannot be suppressed from a plugin. This is not specific to RBAC or to this panel: every plugin's Swing settings panel has the same exposure, because the hook simply does not exist in the Swing client. The Web Administrator does better here; its panels consult the authorization controller directly, so in-panel controls the role does not grant are hidden, not just task-pane items.


Home