Daniel Draes
ServiceNow Employee
ServiceNow Employee

Quite frequently we hit the requirement that certain data in a customer instance needs to be secured / protected against accidental access. The obvious choice and also in-line with technical best practice is to use an Access control rules aka ACL.

 

So far so good, but what if the rule is not as simple as defining a condition or checking a system role? ¨

 

Assume we have users from multiple companies accessing our system. The easy part is to allow access to the records based on company, i.e. Company A can only see their own incidents. But if we have another company - maybe a partner - who also needs access to Company A's incidents we are screwed. Let's see how we can tackle it.

 

 

Allow configuration to define access

Create a new field on the user record to store which companies a user is allowed to see

This field has to be of type glide_list so we can store more than one company.

 

We could also do a separate table if we do not want to pollute / enhance the sys_user record. In the end all we need is a place where we find what records are ok to be seen for the given user.

 

Create Access Control Rule (ACL)

With the persisted information on what a user can access, we can define appropriate Read-ACL using a script to read that information from the user record. While this will work perfectly, it does have a significant impact to the performance. Imagine we load a list of 20 incidents, for each and every incident the system now needs to go back to the user record and find our new custom field. This means we fire 20 new select statements to the database always returning the same value. Of course a cache would probably sort most of it out, but imagine your rule being even more complex. I have seen customer scenarios where the information could not be stored directly on the user record, but on related information like a specific other data set that needed to be loaded. Anyhow, fetching this information for every record is surely not recommendable for a read operation.

 

Store user specific information in session object

With every login to the instance the platform will create a session object. If we store the information we need in here, our ACL has no need to fetch it from the database for every record. Here is how this can be achieved:

With the login the platform will fire an event called 'session.established'. Usually this is used for reporting purposes, but we can leverage it for our case.

 

Create a script action

Create a new Script action listening to our event 'session.established'. There is one important gotcha in getting this to work. Usually all script actions will run asynchronously causing the session to be established properly, but whatever your script does will have no effect to the session. To avoid that we need to change our action to run synchronously. To do so we need to cheat a little. There is a field on our script action table called synchronous (sysevent_script_action.synchronous), but this column is being hidden from us by ACLs. So elevate your privileges to security admin and search of ACLs with name 'sysevent_script_action.synchronous' - you should find two of them. Simple deactivate them for now. As the column is not on the form we will need to add it to our list view, this will not cause any future harm. Once your see it, change it to 'true' for our new script action. Should look like this:

 

script_synchronous.png

Awesome. Now to the script content:

 

var session = gs.getSession();
session.putClientData('test1', 'Harry');

 

With this example we store the value of 'Harry' as property named 'test1'. Obviously this needs to be amended to first find the data we need, like querying the user record for the companies we want to allow.

Change the ACL script

Within our read ACL where we used to lookup the user record we now need to change it to fetch the information from the user session:

 

var session = gs.getSession();
var clientData = session.getClientData('test1');

// now check current record
if (current.company == clientData) {
    answer = true;
} else {
    answer = false;
}

Now we can use the 'clientData' variable to validate the current record. In my example I am using a simple '==' operator, if clientData contains a list of objects make sure you do amend as necessary.

 

Optional: Create Before Query Business Rule

With the ACL's in place we can rest assured our data is safe. However it is still showing the message to the user that certain records are removed due to security constraints. If we want to avoid that we can add a before query business rule. This would look something like this:

  current.addQuery("company", "IN", gs.getSession().getClientData('test1'));

Now the query to the database will be modified before it hits the database even filtering the records in the first place. This will completely avoid the ACL Read scripts as the data is not even there anymore. Much better as we removed unnecessary operations from the execution making the system faster for our end users.

Comments
marcus_redfern
Kilo Expert

Hi - I am about to do some work in this area and was wondering if you had any follow up thoughts or lessons learned on this a year later? also if any developments in the platform capability in the last 12 months have changed any of this advice?

Daniel Draes
ServiceNow Employee
ServiceNow Employee

Hi Marcus,

good question. There are quite a set of features or functionality better which could help make your life easier: Customer Service Management, Application Scopes, Domain Separation, Simple Record Separation (share app) just to name a few.

Which one fits best depends on your exact use case. I personally still use above setup as a good example in new customers.

Fogg

Inactive_Use407
Giga Contributor

Hi Fogg, hope you're well!

 

Thanks for your post.. I'm trying use in my company, but I missed something.. because I really isnt good with codes yet. Can you help me?

I created the u_client_data field in the sys_user table

 

And I created a script action:

var session = gs.getSession();session.putClientData('test1', 'Harry');

 

ACL read:

 

var session = gs.getSession();
var clientData = session.getClientData('test1');
if (current.u_requisitado_por_task.company == clientData){
    answer = true;
} else {
    answer = false;
}

 

I can't get to the right script 😞

 

Thanks in advance!

Daniel Draes
ServiceNow Employee
ServiceNow Employee

Hi g_0710,

As long as your script action has been set to synchronous you have this part correct.

 

In your ACL script though it seems you do a dot-walking to 'company'. In this case the u_requisitado_por_task field would need to be a reference field. I would not recommend doing that as the dot-walking forces the platform to do another database query to get the referenced record.

You really want the information required to determine access rights to be on the current record.

 

bhain
Kilo Contributor

Daniel.Draes could this possibly improve performance if implemented properly rather than hinder it?

Daniel Draes
ServiceNow Employee
ServiceNow Employee

@bhain Haven't looked at it from this perspective. But yes, if your data set is rather large and you make sure the fields used to filter records are properly indexed so that the queries can profit from it, the speed the database can answer queries should improve.

There is probably more to this, especially when you start to add additional conditions to the queries. User can do so by narrowing down their searches or creating reports. Hitting the right indexes will get more complex, I would then recommend to involve a good DBA to make sure the queries produced are optimal.

Version history
Last update:
‎02-14-2018 08:07 AM
Updated by: