Grant Hulbert
ServiceNow Employee
ServiceNow Employee
Part 1 Part 2 (you are here) Part 3

 

Welcome back!

In part 1 of this series, you learned how to configure a simple ServiceNow app to securely restrict access to its data, even from other ServiceNow admins. In this article, you’ll learn how to extend that app’s table security so that only people from certain groups can see specific rows. This is called row-level security.

 

Why would you want to make some rows only visible to certain groups of people? Imagine your company has several departments who want to use your app, but they don’t want to share data with each other. (Perhaps there is some sensitive data that should not be leaked outside a group.) We’ll make it easy to lock down individual rows, so each department has its own private view of your app. As far as your users are concerned, they won’t even know that other people are using your awesome app.

 

Let’s continue!

Prerequisite: make sure you have already followed all the steps in part 1, because we’ll be adding new features to the app we built. You must be logged in as the same administrator that owns the application you built in part 1.

 

  1. Launch Studio, select your app, and select the table “Secure Data”.
  2. Add a column labeled “Group Owning this Record”: make it of type “Reference” and configure it to reference table sys_user_group.
  3. In preparation for editing table roles, elevate your role to security_admin.
  4. Notice there are 4 Access Controls (ACLs) on your table: delete, read, write, and create. In the next steps, you will edit 3 of these, and add 2 new ones.
    1. Edit the delete ACL, adding a condition “Group owning this record is (dynamic) One of My Groups”.
    2. Do the same for the read ACL.
    3. Do the same for the write ACL.
    4. Create a new ACL on your secure_data table, with Operation=read
    5. Tick [x] Advanced, scroll down to the Script section, and paste this script:
      answer = current.isNewRecord();
    6. Select the create ACL. Select hamburger icon > Insert with Roleshamburger.png
    7. Make sure to choose * in the fields column next to the table name.
    8. Click [Update] to save the new create ACL you just made.
  5. Now we’re going to create 2 groups that have access to your app:
    1. In Studio, look under Access Control > Roles, and memorize the name of the secure_data_user role. It should look something like x_NNNN_secure_data.secure_data_user.
    2. Create a new group. (You will need to switch browser tabs; leave the Studio tab open.) Name the group “Secure Group 1”.
    3. Edit the roles, adding your x_NNNN_secure_data.secure_data_user role.
    4. Edit the group members, adding “Abel Tuter” to the group.
    5. Create another group named “Secure Group 2”, and add the same role to it.
    6. Edit the group members, adding “Beth Anglin”.
  6. Go back to your Studio browser tab, and create a new business rule in your app:
    1. Name it “Mark records as owned by logged-in user”
    2. Select your secure_data table
    3. tick the [x] Insert checkbox
    4. Tick [x] Advanced
    5. Click the [Advanced] tab, and replace the entire example script with the following:
      1. (function executeRule(current, previous /*null when async*/) {
        // Set the group ownership of this record to the currently logged-in user's secure group
        var gr = new GlideRecord('sys_user_grmember');
        gr.addQuery('user', gs.getUserID());
        gr.addEncodedQuery('group.roles=x_NNNN_secure_data.secure_data_user');
        gr.query();
        if (gr.next()) {
        current.group_owning_this_record = gr.group.toString();
        }
        })(current, previous);
    6. You will have to modify the script slightly. On line 6, change x_NNNN_secure_data.secure_data_user to be the exact name of the secure_data_user role you assigned to the “Secure Group 1” group
  7. Try it out!
    1. Log out, and log back in as user “Normal Admin” (remember creating this user back in part 1?)
    2. Notice you cannot see your app in the left nav.
    3. While still logged in as “Normal Admin”, impersonate System Administrator (username=admin).
    4. Notice you cannot see the app’s tables. Notice you cannot add “Secure Group 1” to your own groups. Notice you cannot add any secure app roles to your roles. Why not? You would think impersonating System Administrator would confer all privileges, including “Application Administration”. Answer: because you’re not really logged in as that user; you’re merely impersonating, and ServiceNow’s engineers were smart enough to prevent this potential security breach!
    5. Impersonate Abel Tuter; notice you cannot see the app or any of its records. Why not? Again, you’re merely impersonating Abel, and because this app is so tightly secured, you are not allowed to see his records, even when you’re impersonating.
    6. Navigate to Scripts - Background and create a script that queries records from your table; notice you can see all records, regardless of “Group Owning this Record”. Because background scripts run at a very low level without ACLs, they are able to avoid security restrictions. Even though admins will have to go to great lengths to see your app’s data, you should be aware that it’s possible.

Conclusions:

  1. Regular users cannot see data that does not belong to one of their groups, even though all the data is stored in a single table. This is analogous to Domain Separation, but of course, not nearly as sophisticated!
  2. Users who do not have app roles cannot see any data at all.
  3. “Other” Admins cannot edit the app in Studio, and cannot add themselves to groups or roles in the app.
  4. Maint user can see everything; that’s what Maint is for 🙂
  5. “Other” Admins can see data if they go out of their way to run Scripts - Background.
  6. Casual admin activity will not see the data

There's more!

In part 3, the conclusion to this series, we'll add more polish to the app by creating a user management feature that makes it easy to assign users to groups, and disambiguate users who belong to multiple secure groups.

 

Part 1 Part 2 (you are here) Part 3
Comments
Roman Haas
Giga Guru

Hi @Grant Hulbert , thank you very much for this wonderful article. However I have one question. On another topic (from 2013, Link to ServiceNow Community Article ) I read that we shouldn't use the "roles" field on the "sys_user_group" and "sys_user" table (posted by a ServiceNow employee).

 

What do you think about it? We can use this field without concern?

Grant Hulbert
ServiceNow Employee
ServiceNow Employee

Hi @Roman Haas I'm not seeing a connection between that article and my technique. My code is not using the roles field, and that 2013 article seems to be referring to a form field that was leftover from a very old release of ServiceNow.

I could certainly be wrong about that article, but I do know that we are using my code in production on ServiceNow's own internal IT instance without issue.

Roman Haas
Giga Guru

Hello @Grant Hulbert, I see there a connection with your business rule script.

 

In the article (Link to ServiceNow Community Article) the author describes a question around the sys_user_group.roles field. The first answer from @Mark Stanger is saying "It's a legacy field that hasn't been used for several years. It should be removed from your form and never be used."

 

In your article, step 6.5 you are querying the sys_user_grmember table with following query:

gr.addEncodedQuery('group.roles=x_NNNN_secure_data.secure_data_user');

 

group.roles refer to the roles field from the sys_user_group table (as the group field on the sys_user_grmember is a reference to the sys_user_group table). Hence I assume you are querying a field that, according to the answer from Mark Stanger, shouldn't be used anymore.

 

I hope I am not making a mistake here. I'm not looking for mistakes, I find your article great and very helpful. I just happened to stumble across the other article.

 

Grant Hulbert
ServiceNow Employee
ServiceNow Employee

Thanks for the clarification, @Roman Haas -- I really appreciate you digging in to this. While I have not run it past the platform team for a deep dive, I'm going to assume there's a subtle distinction between that article's focus (which was to make sure the group_roles field is *hidden* on the form), and my use of it. My educated guess is that the field is indeed kept up to date properly by the platform (under the hood), thus making it OK to query, but that it would be a bad idea to let admins edit that field willy-nilly.

 

From my previous life in the platform team, I recall we often did 'flattening', where we would aggregate data into a single field in order to make subsequent queries faster. The platform would ensure the field always had correct data in it, but it would be a bad idea to let users touch it. I do see other code patterns that look like this in our codebase.

 

I suppose if this is indeed wrong, at least the same principle can be applied by using a slight code tweak to query child roles, which would bypass any potential issues.

Daniel Draes
ServiceNow Employee
ServiceNow Employee

I would indeed be a bit careful with the 'roles' field on group table. Did a quick test on my instance:

 

1. On a given group I made sure it has a role: ATF_TestGroup_Network

2. Queried that group on a background script and report on the roles-field: Empty.

var group = new GlideRecord('sys_user_group');
group.get('3cc3c7680b982300cac6c08393673a03');
gs.info('Roles:' + group.roles);
---
*** Script: Roles:

To be on the safe side, I also used the way to query via the group membership table:

var gr_member = new GlideRecord('sys_user_grmember');
gr_member.addQuery('user', 'bba5cf680b982300cac6c08393673a42');
gr_member.query();

while (gr_member.next()) {
    gs.info('Group: ' + gr_member.group.name + ' - ' + gr_member.group.roles);
}
----
*** Script: Group: ATF_TestGroup_ServiceDesk - 
*** Script: Group: ATF_TestGroup_Network - 

As we can see, now role name is returned if though one of the groups does have a role assigned.

 

Now, when I extend the query to the groups table with the encoded query, it actually does work correctly:

var gr_member = new GlideRecord('sys_user_grmember');
gr_member.addQuery('user', 'bba5cf680b982300cac6c08393673a42');
gr_member.addEncodedQuery('group.roles=atf_test_admin');
gr_member.query();

while (gr_member.next()) {
    gs.info('Group: ' + gr_member.group.name + ' - ' + gr_member.group.roles);
}
----
*** Script: Group: ATF_TestGroup_Network - 

It still will not return the role name, but it did filter down to only the group which has the role.

 

So keep that in mind when you implement a solution like this. Debugging and Testing can become really difficult.

 

Daniel Draes
ServiceNow Employee
ServiceNow Employee

Another aspect while thinking about this in more detail is surely the impact to performance.

Adding logic which requires scripting to a READ ACL has to be given extra care. Well, this one does not directly add a script to it, but the qualifier “Group owning this record is (dynamic) One of My Groups” is indirectly a script, as 'One of My Groups' involves some lines of code - namely:

gs.getUser().getMyGroups();

 

This means every time a record is read from the database, this code has to execute. While on a single record the impact is negligible, on a list of record the story is different. If the impact on one give record is like 5 ms, on a list of 20 we already accumulate 100 ms.

 

The next part to it is that on large organisations users might be in a lot of groups where only a few are 'security' type groups. Given the database all groups a user is member of adds more load without any help. So I would recommend to filter the list of groups to only the relevant ones before passing it to the query. In the 3rd part of your series you show one approach to it - great. I would take it even one step further. See an article I wrote some months ago:
HowTo secure data access without impacting perform... - ServiceNow Community

Version history
Last update:
‎02-15-2023 11:36 AM
Updated by:
Contributors