Trying to read user roles from the session results in different values when using binding/script

Hi All,

I am attempting to implement a custom permission system in our Ignition app.

I have implemented a script to verify that a user role matches against a permission in our database. The user role is taken from the Ignition users.

I have this working when running the script via a "runScript" expression, and I pass the permission key, and user roles list to the function. No problem. As seen here:

runScript("permission.isAuthorised", 0, "LOUNP", {session.props.auth.user.roles})

This calls the following code:


def roleHasPermission(role, permission):
	
	if role == "Administrator":
		return True
	
	query = "Permissions/SelectUserPermission"
	params = {
		"role": role,
		"permission": permission
	}
	result = system.db.runNamedQuery(query, params)

	return result > 0

def isAuthorised(permission, roles):

	hasPermission = False
	try:
		values = [qv.value for qv in roles]
		for role in values:
			hasPermission = roleHasPermission(role, permission)
	except:
		pass
	
	try:
		if not hasPermission:
			return not system.db.runNamedQuery("Permissions/SelectIsEnforced", {"key": permission})
	except:
		pass
		
	return hasPermission

This works fine, the list of roles passed in is of type:

array(com.inductiveautomation.ignition.common.model.values.QualifiedValue, ...)

My problem is when I try to get the same list of roles from a transform script the type I receive is now different.

def transform(self, value, quality, timestamp):
		
	if not permission.isAuthorised("STSCR", self.session.props.auth.user.roles):
		return False
		
	return value

Using "self.session.props.auth.user.roles" results in a list of type ArrayWrapper:

<ArrayWrapper>: [u'Administrator']

Is this supposed to be the case, or do I need to retrieve the roles from somewhere else? I'd like both arrays to be of type

array(com.inductiveautomation.ignition.common.model.values.QualifiedValue, ...)

I am assuming that it won't be easy to convert an ArrayWrapper to my desired type.

Any guidance is appreciated.

Thanks,
Tom

that desired type is a bit challenging, but if all you need it to do is match the typing schema in isAuthorized, then this should be easier.

per PropertyTreeScriptWrapper.ArrayWrapper
docs, this supports indexing into, so (Pseudocode)


	hasPermission = False
	try:
                if isinstance(roles[0], QualifiedValue):
		            values = [qv.value for qv in roles]
                else:
                    values = [value for value in roles]
		for role in values:
			hasPermission = roleHasPermission(role, permission)
	except:
		pass
	
	try:
		if not hasPermission:
			return not system.db.runNamedQuery("Permissions/SelectIsEnforced", {"key": permission})
	except:
		pass
		
	return hasPermission

should work

1 Like

Ah, perfect, that works great.

Thanks for the quick response!

Tom

1 Like

There's a subtle logical bug here - if you pass a list of [roleThatHasPermission, roleThatDoesn'tHavePermission], you'll get a false return, but if you flip the order of that list you'll get a true return.

For this and other, mostly stylistic reasons, here's how I'd write this code:

def isAuthorised(permission, roles):
	hasPermission = False
	try:
		# using any so that only one role has to be valid to pass -
		# use all instead to require that all provided roles have permissions
		hasPermission = any(
			roleHasPermission(getattr(maybeQv, "value", maybeQv), permission)
			for maybeQv in roles
		)
	except:
		return False

	return hasPermission || not selectIsEnforced(permission)

def selectIsEnforced(permission):
	try:
		return not system.db.runNamedQuery("Permissions/SelectIsEnforced", {"key": permission})
	except:
		return False

However, I'll also mention that the idea of writing your own auth checking routines feels fragile and potentially misguided. You're already in an expression context - why not use isAuthorized?

Hi Paul,

Thanks for the reply. I will check over the logic and implement your changes.

I understand that using our own authentication is not good practice. We were using the in-built Ignition security features.

Unfortunately, our customer wanted more precise control over the permissions of the HMI.

For example, we have a number of roles in Ignition; Operator/Technician/Engineer/Administrator. Our application requires that a permission be available for clicking a button for example. Currently, we have to determine which roles we think should be allowed to click said button.

We then have to write an expression to check "isAuthorized" for said roles. This is ok, but the customer wants to assign that permission to a role dynamically, or turn off that permission all together. I couldn't see a way to do this.

The limit is, if we wanted a Technician to be able to click a button, then we can do that in an expressions, but once we hand over the system to the customer and they no longer want a Technician to be able to click that button, they have to change the expression themselves.

I hope that I have explained that well enough, any advice would be greatly appreciated.

Thanks,
Tom

Precisely. You should make them do so. Any roll-your-own system that has button-by-button configurability will certainly be a security nightmare.

Indeed. Unfortunately, we have to go down this route.

Doing a bit of digging after Paul's comments. I think it would be possible to use "Security Levels" and "Roles" to achieve this. Then we could use Ignition to handle security.

I just need a way to dynamically assign "Roles" to "Security Levels"...

What does the button do? Does it write to a tag? If so, use write permissions on the tag itself for this and bind to the tag's canWrite prop to disable the button

It sends some messages to a module that we have running in Ignition. It is for process control of a factory line.

1 Like