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

Just to clarify - the only part I’m wanting to outsource to the script is the “does the currently logged in user have this user role” part. That’s the part that gets used again and again - the Maint_Mode is just an example, there could be any number of other conditions I want to check.

I’ve created a shared script called security, and defined several functions along the lines of

def checkMaint(roles)
    return roles is not None and 'Maint' in roles

Then I’ve put an expression binding on the visibility of the item I’m looking to control:

runScript('shared.security.checkMaint', 1000, {session.props.auth.user.roles})

This works, and I could then add a && {'Maint_Mode'} afterward if needed. It’s still more complex that I’d like (it’s 5 seconds work to type hasRole('Maint'), whereas this one will likely be a copy binding/paste binding/edit the role I’m interested in) but it’s the neatest option I’ve found so far.

My only question is, if I have a view with several components bound in such a way, will there be any issues with the fact that each one of them will be running that script every second over and over again? It’s a very simple script, but it still seems like a waste to run it over and over. Is there any way to trigger it to only re-run when the user changes?

1 Like

If you are passing arguments to runScript, it'll execute on arg change in addition to timing. So you can set the interval to zero to get what you want.

1 Like

I'm going to have to correct myself there - this doesn't work. It "worked" inasmuch as the expression compiled and returned me a valid parameter (false, because there is no user in the designer) - but now that I have users set up and am logging in from a session, it doesn't work. I have put some text on the screen bound to my user roles, so I'm confident that the user has the Maint role. If I bind to the {session.props.auth.user.roles} parameter and then use a script transform

return shared.security.checkMaint(values)

as suggested by @cmallonee, then it works, so my script is obviously working and returning the correct value for the given user role. But my runScript expression doesn't do the same.

Any idea what I'm doing wrong? I can use the script transform method for most of my use cases, but when I need to also add a tag-based condition into the mix, I'm forced to revert to an expression, so I need to be able to make it work that way as well.

Have you tried

runScript('shared.security.checkMaint()', 1000, {session.props.auth.user.roles})

Yes - that gives me an expression evaluation error.

Ooof, okay, you’re not going to like what I had to do:

Expression Binding:

runScript('project.Security.hasAdmin',0,{session.props.auth.user.roles}) && {[default]Experimentation/ExpBoolean.value}

Script:

def checkMaint(roles):
	hasRole = False
	if roles is not None:
		for role in roles:
			if role.value == 'Maint':
				return True
	return False

roles is a QualifiedValue, which means you need to directly address the internal “value” property.

1 Like

That explains why it works in the script transform on the property binding - I’m passing the property value to the script, but the runScript function is passing the whole property. That means that I can set up a script to be used on a script transform, or an expression, but not both. Ooof.

I think that despite my best efforts at avoiding it, my best shot is going to be reverting back to the hasRole function and a straight expression:

{session.props.auth.user.username} != null &&
hasRole('Maint', {session.props.auth.user.username}, 'default') &&
{'Some_Other_Tag'}

Really not a straightforward operation any more, and I still don’t like the way it will break if I ever switch to a different user source.

Otherwise - is there no way of passing the property value with the runScript command? e.g.

runScript('project.Security.hasAdmin',0,{session.props.auth.user.roles.value})

…compiles correctly but again doesn’t return true when logged in.

Or, am I just going about this all the wrong way? Are user roles not the best way to determine authorisation in perspective? Should I be using the new security levels instead? As I keep banging on about - there’s got to be an easy way to check user permissions, maybe I’m just trying to fit a square peg in a round hole, and haven’t noticed the shiny new square hole behind me.

User “roles” and security levels are meant to be used together to determine permissions, although there’s nothing to stop you from creating a security level which implies a user has a certain role.

You could easily make a security level called “Maint” and make a check to see if the user has that security level, but the process is almost identical in that session.props.auth.securityLevels and session.props.auth.user.roles are both arrays, and so will both need to be accessed the same way (for loops in a script, checking against value[x].value). In looking at session.props.auth.securityLevels, it’s kind of a mess right now, and I’m not sure if it’s bugged, or if the current structure is expected (I’ll have to get back to you).

A potential way to ease the user source concern is to create a session.custom property which stores the expected user source for the project (assuming you will only ever use one at a time for the project). You could then reference this property in any expressions, and if you ever need to change it you only need to change it in one place.

The {session.props.auth.user.roles.value} would compile because the expression has no insight into the structure of the session object (so it doesn’t know that roles.value doesn’t exist) but I would expect it to return False because roles will equate to None (because roles.value doesn’t exist).

Okay. I’ve found myself a much less headache-inducing way around my problem (I think!)

  1. Go to the session properties editor and add three custom properties: hasAdmin, hasMaint and hasOperator
  2. Under the standard session properties, drill down through auth/user, right click on roles and add a change script
  3. Add the following script:
self.custom.hasAdmin = 0
self.custom.hasMaint = 0
self.custom.hasOperator = 0
if currentValue.value is not None:
    for role in currentValue.value:
        if role.value == 'Admin':
            self.custom.hasAdmin = 1
        if role.value == 'Maint':
            self.custom.hasMaint = 1
        if role.value == 'Operator':
            self.custom.hasOperator = 1

Whenever the roles are changed in the session properties - be that because of a login, or because user roles were updated in the gateway - the custom session properties will automatically update, giving me a very simple boolean session property I can use in expression bindings without resorting to any sort of scripting, transform, null checking, or array iteration.

Obviously, this is somewhat insecure, because these session properties could easily be manipulated by a script etc. For my particular application this likely won’t cause any issues - if someone has access to manipulate my scripts, they’ve probably also got access to remove the expression binding checking for the login anyway.

I could always add the same change script to each of my custom properties (or make the script a shared one, and have them all call the shared script), which would mean that even if someone did find a way to write to these custom properties, as soon as they were written to, they’d reset themselves against the login regardless. Although I’d probably have to modify the script somewhat, because otherwise it would probably get itself caught in a loop when it sets all of the custom properties to zero initially before checking roles and setting the correct ones back to 1.

Any thoughts on this approach? From a security, optimisation, or improvement point of view?

Would it be a reasonably straightforward idea to implement something similar at the base level, where it didn’t have to be manually scripted, and could probably be made more secure than I’ve made it?

2 Likes

About the only thing I’d say is don’t worry about the security of the scripting side of it, other than pathways to create inputs, because all scripts run on the gateway only, no part of that is ever on the client. They’d already have to be logged directly into the server, or exploiting some sort of Ignition security hole, and at that point they can run whatever they want, they wouldn’t need to futz with your scripts.

1 Like