The number that matters here is the count of user sources in which a given username can resolve. If that count is one, every role check you write is unambiguous. If it is two — an AD/Hybrid source with a local source in soft fallback — then hasRole("Administrator", user, "default") answers a question about the string admin, not about the person holding the session. That is the whole failure. A customer-managed directory only has to contain a matching name and role for the check to pass, and the component unlocks for the wrong operator.
The checks engineers reach for first, and why each one misses
Every approach in the list below returns a plausible-looking boolean. None of them carries the identity of the profile that actually authenticated the session.
| Attempted check | What it actually returns | Why it fails the fallback case |
|---|---|---|
hasRole("Administrator", user, usersource) with an explicit source |
True if a user of that name in that named source holds the role | A duplicate name created in the customer-managed source satisfies the lookup regardless of which source logged the session in |
hasRole("Administrator") with the source left blank |
Role check against the current user's source | Correct source, but it exposes only the role — the source name itself is never surfaced to the expression |
system.security.getRoles() |
Role list for the current session | Roles from AD and roles from local users are the same strings; nothing distinguishes the origin |
[System]Client/System/UserSource |
The user source configured on the project | Static per project. With a fallback chain it names the primary source even when the session authenticated against the fallback |
| Security levels and security zones | Gateway-side security level tree | Not available for restricting Vision component access; the check has to happen in client script |
Setting the role names identically in both sources is the aggravating factor, not the bug. A duplicate admin account with an Administrator role in the directory is indistinguishable, to any of those five checks, from the local emergency account that was supposed to be private.
How soft fallback resolves a login
A soft-fallback chain authenticates against the primary source first. If the primary is reachable and rejects the credentials — or the account does not exist there — the gateway tries the fallback source. The session that results carries a username and a role set, and the profile that produced it is recorded on the user record itself. It is not published to the client as a tag or an expression variable.
That design is why the pattern works so well as a contractor-access mechanism on an AD-controlled system: integrator accounts live only in the local source, the customer owns the directory, and neither party edits the other's list. It also explains the exposure. Ownership of the directory means the customer can create any username and assign any role that exists in the project. If your privilege check keys on name plus role, the customer can mint that pair.
The identifier that decides: profile name on the User object
The value you want is the profile name attached to the authenticated user record. In the Java API, com.inductiveautomation.ignition.common.user.BasicUser exposes getProfileName(), and that string is the name of the user source the record came from. Every other approach is a proxy for it.
The practical difficulty is obtaining a User object for the current session in Vision client scope rather than a role list. The client-side security functions hand you a username and roles; getting to the object means a lookup. Retrieve the record from each candidate source in turn, cast-check it, and read the profile name from whichever source returns a record.
Procedure: resolving a profile name in a Vision client script
- Enumerate the sources in your fallback chain explicitly, primary first, fallback last. Hard-coding this list is correct — the chain is a design decision, not runtime data.
- Read the current username from the client security functions.
- Look the username up in each source in order and stop at the first record returned.
- Test the returned object for
BasicUserand readgetProfileName(). - Gate the component on both the profile name and the role, never on the role alone.
Bind the component's enabled or visible property to a script that calls mayEdit(), and re-evaluate it on login rather than on a fast timer — the lookup crosses to the gateway and, for a directory source, to the domain controller.
Understand what this code proves and what it does not. It proves which source the lookup found the name in first. When the name is unique across the chain, that is identical to the source that authenticated the session. When the same name exists in both, the ordered lookup returns the primary and the local emergency account is correctly denied — the failure mode is fail-safe rather than fail-open, which is the opposite of the hasRole behaviour. It still does not distinguish two genuinely distinct people who share a name.
Removing the ambiguity at the namespace level
Script-side detection is a mitigation. The durable fix is making the duplicate impossible or harmless. Three designs do that, in increasing order of robustness:
-
Suffix the namespace. Name privileged local accounts in a
username@suffixform that your directory naming policy cannot produce. A collision then requires the customer to create an account that violates their own directory conventions, which is visible in an audit. -
Disjoint role vocabularies. Define the roles that unlock the protected components only in the local source, and never publish them to the directory-backed source. Give the customer the right to assign roles from a fixed list, not to create roles. A directory user then cannot hold the role at all, and the plain
hasRole()check with a blank source becomes sufficient. - Separate the concerns entirely. If the intent is engineering access, keep it out of the operational role model — a dedicated project, a launched client with its own project user source, or gateway-scoped controls that the customer's directory cannot reach.
The second option is the one that scales. It limits the customer to administering their own source, which is what they wanted, and it removes any dependency on knowing the profile name at runtime.
Verification
- Create
adminin the local source with the privileged role and a known password. - Create
adminin the directory source with a different password and the same role name. This is the exact collision the design must survive. - Log in with the directory credentials.
currentProfile()must return the primary source name and the component must stay locked. - Log in with the local credentials. If the duplicate exists, expect the same locked result — that is fail-safe, and it tells you the namespace is contaminated. Remove the duplicate, log in again, and confirm the profile name resolves to the fallback source and the component unlocks.
- Disconnect or block the domain controller and repeat the local login to confirm the fallback path still resolves the profile name the same way.
- Log a line with the username, the resolved profile, and the role list on every successful unlock. Without that record you have no way to reconstruct who opened the component six months later.
Pitfalls that bite later
Fallback is a backup mechanism. Using it as a second, hidden user source is workable, and it is a common contractor-access pattern, but it is unconventional enough that the next engineer will not infer it from the configuration. Document the chain order, the privileged source name, and the role vocabulary split in the project itself, because the entire security boundary lives in that ordering.
Two more sharp edges. The hard-coded source list in the script must be updated whenever the chain changes — a renamed source silently makes currentProfile() return None and locks everyone out, which is safe but confusing. And a client-side script gate protects the UI, not the data: anything reachable through gateway-scoped scripts or tag writes needs its own check, because a component that is merely disabled is still backed by tags that accept writes.
When to escalate
If the ordered lookup returns a profile name that contradicts the credentials that were actually accepted — for example the fallback source is returned for a session that authenticated against the directory with no duplicate present — stop scripting around it and open a ticket with Inductive Automation support, with gateway logs covering the login and your user source configuration export. The same applies if a directory user resolves to the local profile during a controller outage. Those are authentication-path questions that belong with the vendor, not with an application-layer workaround.
FAQ
How do I get the current user's user source in a Vision client script?
Look the current username up in each configured source in fallback order using system.user.getUser(), then read getProfileName() from the returned com.inductiveautomation.ignition.common.user.BasicUser. There is no client tag or expression function that publishes the authenticated profile directly.
How do I stop hasRole() from matching a duplicate user in another source?
You cannot — hasRole("Administrator", user, usersource) tests a name and a role in the named source and knows nothing about the session's origin. Make the role exist in only one source, or suffix privileged usernames so a directory account cannot reproduce them.
Why doesn't [System]Client/System/UserSource give me the right answer?
That tag reports the user source configured on the project, which is static. With a soft-fallback chain it names the primary source even when the session was authenticated by the fallback.
How do I give contractors admin access on an AD-controlled system without letting the customer create admins?
Keep the AD/Hybrid source as the primary with a local source in soft fallback for the contractor accounts, and define the privileged roles only in the local source. Restrict the customer to assigning existing roles rather than creating them, so no directory account can ever hold the privileged role.
How do I verify that my component gate actually blocks a duplicate account?
Create the same username with the same role in both sources, log in with the directory credentials, and confirm the component stays locked and your resolved profile name reports the primary source. Repeat with the domain controller unreachable to confirm the fallback path behaves identically.