[Feature - 12943] hasRole() Runtime Exception: Function does not work with 1 arguments

When trying to use an expression binding to enable or disable a perspective component based on user roles, using the following expression:
hasRole('Service')
I get the error
RuntimeException: Function 'hasRole(role, username, usersource)' does not work with 1 arguments
The documentation for this function shows that username and user source are optional in the client scope, but required in the gateway scope. I understand that I’m developing on the gateway, but I used this function extensively on v7.9 and never had this issue

Am I doing something wrong, or do I need to handle user roles differently in perspective? I don’t want to pass the usernames if I can avoid it because I don’t care which user is logged in, I only care what roles they have (that’s the whole point of roles, after all).

In Perspective, ALL scripting functions execute Gateway side. Due to this, when you’re examining scopes and execution you should consider all functions/methods/calls made in Perspective to be Gateway scoped/executed.

As the Gateway scope requires all three arguments, you must provide all three arguments in Perspective if you want to use this function.

If you only want to know if the current user has a role, you could avoid using the function altogether and instead use the session.auth.user.roles property in the session properties.

3 Likes

@cmallonee is correct from a technical standpoint.

I think this is worth discussing internally though to see if there’s anything we can do, much like we implicitly provide the project parameter to many gateway-scoped scripting functions in 8.0.

Edit: meh, Cody’s update pointing out the property is available obviates the need.

That makes sense. Is there any documentation available yet for session.props? If not, can you give me any pointers on what expression function I might use to determine whether my user has a given role? I’m presuming it’s not just as simple as session.props.auth.user.roles = 'MyRole' since presumably roles would be a list or dictionary containing every role assigned to the currently logged in user?

You should bind against the session.auth.user.roles property, then use a script transform:

if 'myRole' not in value:
    self.props.enabled = False

That seems more complicated than just using hasRole(), even if I do have to pass the user name in. Especially if I have multiple conditions for enabling, and the user role is just one of them. e.g. the old way would be as simple as
hasRole('Admin') && tag([default]System/SetupMode)

Perhaps I could just pass the current user from {session.props.auth.user.userName}? It doesn’t look like I can do the same with the user source, although in my case I can just specify ‘default’ so that would work in my particular instance. When I try this, I get an ExpressionEval Error (user ‘null’ not found in user source ‘default’) but presumably it would work at runtime once I get my user management structure set up?

It should work at runtime. The Designer doesn’t populate the session.props.auth properties because there is no authentication taking place.

Thanks, I’ll get my user management set up in the next couple of days and see how I go.

I did just have the thought though - unlike the Vision module, it looks like I don’t have to log in to access Perspective views. So, when I first access the displays, the current username is null and the expression will once again error out.

I suppose it could likely be overcome by a startup script or something to log in a default user, but we’re starting to again get into the territory of significantly overcomplicating what should be a very simple check. Checking user roles is a very frequent operation in all the SCADA systems I’ve used - I’d like to think it could be implemented in a more straightforward manner.

You’ll need to account for those possibilities, which is part of why hasRole() won’t just “work” for Perspective Sessions; Vision required a login (and so you knew about the user’s authentication profile immediately), whereas Perspective content is as secure as YOU make it.

Also - just to clarify - you don't have to allow public access to your perspective project. You can require authentication to get to anything, just like how Vision works - just change the Project Properties -> Perspective -> Permissions to not allow the 'public' security level. You will also want to make sure to select an identity provider on the Perspective -> General page.

Thanks, I’ve checked the Authenticated box and now it redirects me to log in at launch. A few follow up questions on this:

  1. My project properties window doesn’t show the roles under the authenticated checkbox like yours does, despite the fact I have several users and roles configured in the gateway. Any idea why that might be?

  2. What kind of startup script might I add to the session events to automatically log in a default user to a perspective session? The old system.security.switchUser('defaultUserName','defaultPassword') doesn’t appear to work for perspective, and I haven’t been able to find any documentation yet on perspective authentication scripting. Typing session. and pressing Ctrl+Space doesn’t yet give me any list of functions to investigate either.

  3. When working with the Vision module, the designer uses whatever user I logged into it with for the “live design” - i.e. if I’m logged into the designer as admin, and I set something to only be visible on admin login, it will appear visible during development. In the perspective module, as @cmallone says, “The Designer doesn’t populate the session.props.auth properties because there is no authentication taking place.”
    This just means that while it will work at runtime, any which way that I design my user restrictions, they’ll show up with an error in the designer, which is less than ideal. Is there any way of having the designer pass its authentication to the perspective design environment?

1 Like
  1. Security levels are not really the same as roles - being perfectly honest, I don’t know much of anything about how best to configure the IdP stuff, but you will need to add security levels on the dedicated ‘security levels’ page in the configure section: http://localhost:8088/web/config/security.security_levels

  2. There is no way to do this currently, and honestly it may or may not be possible to implement. A major factor if identity providers is that they can be run offsite, and require direct user interaction to log in. I’m definitely not an expert, though - I’ll let someone more qualified chime in with more details on that.

  3. Agreed that there’s a gap in functionality there - I filed an internal ticket to add the ability to log in, or else just spoof a login, within the designer. That ticket is 12943, but I won’t attach it to this thread since that’s not quite the overall issue of the rest of the thread.

2 Likes

A driving force behind Perspective is security, and this is a huge security issue. "If everyone is special, then no one is."

The notion of a default user in Perspective is very much the unauthenticated user, and so if your goal is to have everyone sign in as a default user, and apply settings in specific places which check against roles, then you should design everything to NOT check authentication except for the Views/Components/Actions for which you require security.

If you want everyone on a specific sub-net to have access, then you can configure Security Zones.

This will never give you a list of functions, as session is not a package or library - it is an object which has only attributes (properties) for accessing.

1 Like

Fair enough - I guess the thrust of what I'm getting at is that it's become a lot more complex to check user credentials than it is in the vision module (or any other SCADA package). I can leave the system without a default user and only check authentication where I specifically need to, and that's probably the direction I'll take. But to use the example again of needing to check that a user has a maintenance login, and that the machine is in setup mode, it's two parameters - the role and the tag.

hasRole('Maint') && tag('Maint_Mode')

Whereas in Perspective, if I don't have a default user, I have to (1) check that the current user is not null, (2) pass three parameters to a role check, and then (3) check the tag value

if({session.props.auth.user.userName} = null, false, if(hasRole('Maint', {session.props.auth.user.userName}, 'default') && tag('Maint_Mode'), true, false))

It works, but it's way more complicated than any other method of security vetting I've ever used. As you say, Perspective is only as secure as I make it, so I'd like to think that there could be a less tedious way for me to make it secure. If this is how it works for now, then I can work with it - I guess my feedback would just be, surely it could be streamlined somehow?

1 Like

One idea - could I perhaps lump all of this into a common script somehow, and then just call the script? So my expression would become something like

runScript(checkRole,'maint') && tag('Maint_Mode')

…and then the script would handle the null checking etc, something like

def checkRole(role)
    if role is not null:
        if role in {session.auth.user.roles}:
            return true
        else
            return false

Could something like this work, without having to copy the script to every component/view? Presumably I’d have to specify the user I’m checking roles against at somewhere, or would that be implied by the user triggering the script?

Something of this nature would definitely work, and is probably best* as it allows you to place all of the logic in one place, instead of replicating it over and over again.

You’d probably best be served by

def checkMaintenance(roles):
    maintenance_role_str = 'Maint'
    paths = '[default]Maint_Mode'
    return (roles is not None) 
        and (True == system.tag.readBlocking(paths)[0])
        and(maintenance_role_str in roles)

*although in the event your role is null, your script example would return nothing.

1 Like

Worth noting you can simplify that expression by a lot:
hasRole('Maint', toString({session.props.auth.user.userName}) && {'Maint_Mode'}
will compile and evaluate to the same result. You don’t need an if statement if your expression is already returning a boolean value, which the double ampersand AND operator will do. And you don’t need tag() to read a tag, you can just use the curly brace syntax (which will also be faster).

2 Likes

What’s your take on where I’d be best to place this script? In the Project Library shared scripts, or elsewhere?

I still get the evaluation error if no user is logged in, because "null" is not a valid user. As far as I can tell, I must pass a valid username to the hasRole function, or it will error out. If I'm not able to log in a default user automatically, I have to be able to handle the case of there not being a user.

Thanks for the tip on the tag() function though - I'm not sure why I thought that was necessary, maybe because I found it in the functions list and assumed it was the correct way to read a tag? Knowing that it's not required simplifies a lot of expressions for me! :slight_smile:

(edit: I remember now, most of the time I'm looking at tags in an expression, I'm looking at members of a UDT, called indirectly with a tagPath parameter. So I have to use the tag() function to build the complete tag path)

Definitely in the Project Library because you might want to use it in other Views within the project.

If you could see yourself using it in other projects as well, then you should instead put it into the shared scripts portion of your parent project if you have one. If you see yourself using this script in other projects but do not have a parent project, now might be the best time to start one.

As with any other sort of software design, push the shared logic up as high as you can to avoid duplication and ensure uniformity.

1 Like