Monday, December 14, 2015

Access Controls to Allow Only Inserting

At some point, you may need to set up a table that allows users the ability to insert records without being able to read the table.

This could arise with a public feedback form in a mobile app that uses ServiceNow as the back-end. Since users are not required to be authenticated to submit feedback, the app might use the same guest account (with no roles) for all users' submissions via JSONv2. Should someone hack the app and discover the guest credentials, you wouldn't want them to be able to obtain historical feedback.

Setup


Suppose you have a table, u_feedback with a single field, u_description (in addition to system fields.)
+---------------+
| u_feedback    |
+---------------+
| u_description |
| ...           |
+---------------+
Along with this, you have a guest account:
Username: app.guest
Password: 1234
If you attempt an insert using JSONv2, an error will be thrown because you haven't created any Access Controls. At this point, app.guest can't read, write, insert, or anything. As shown using curl on the command line:
$ curl --user app.guest:1234 --header 'Content-Type:application/json' --data '{"description":"Great App!"}' "https://instance.service-now.com/u_feedback.do?JSONv2&sysparm_action=insert"
{"error":"Insufficient rights to insert records from the table: u_feedback","reason":null}

Putting in create, write ACLs


Adding two blank Access Controls resolves this.
Access Control 1
Type: record
Operation: create
Name: Feedback [u_feedback] | -- None --

Access Control 2
Type: record
Operation: write
Name: Feedback [u_feedback] | -- None --
The blank create ACL will allow app.guest (or any authenticated user) to insert records into u_feedback. The blank write ACL will allow u_description to have a non-blank value. You may be wondering whether the write ACL also allows anyone to issue a bulk update to the entire table using JSONv2 sysparm_action=update. It does not. Such an update would initiate a query first, which itself would require read privileges that are not granted (yet).

Now, if you attempt an insert again, the record will actually be inserted correctly. But, a different error will be thrown. This is because upon inserts, JSONv2 typically "echoes" the inserted record back to the requester. And, app.guest does not have the ability to read the table.
$ curl --user app.guest:1234 --header 'Content-Type:application/json' --data '{"description":"Great App!"}' "https://instance.service-now.com/u_feedback.do?JSONv2&sysparm_action=insert"
{"error":"Insufficient rights to query records from the table: u_feedback","reason":null}

Allowing JSONv2 to echo inserts


The above error is misleading and annoying even though the insert was successful. In a more advanced situation, like inserting into an Import Set table, you'd want to echo the inserted record because it would contain important status and error messages. To enable echoing without compromising the overall non-readability of our table, you need to allow reading of inserted records only by the same HTTP request that initiated the insertion in the first place.

To do that you'll leverage a global variable that survives through every server-side script executed by a HTTP request, including advanced Business Rules and Access Control scripts. Start by capturing the sys_id of the inserted record in a Business Rule:
Business Rule
Name: Capture sys_id on insert
Table: u_feedback When: before
Insert: true
Advanced: true
Script:
var com_acme_feedback_app_insert_sys_id;
function onBefore(current, previous) {
  com_acme_feedback_app_insert_sys_id =
    current.sys_id.toString();
}
Then, set up a read ACL and leverage the captured sys_id:
Access Control 3
Type: record
Operation: read
Name: Feedback [u_feedback] | -- None --
Advanced: true
Script:
(current.sys_id.toString() ==
  com_acme_feedback_app_insert_sys_id)
When u_feedback is queried immediately following an insertion, com_acme_feedback_app_insert_sys_id will contain the sys_id of the inserted record and ACL script will return true. Any other time the table is queried, the ACL script will return false.

The effect of the Business Rule and Access Control can be seen by issuing another insert request:
$ curl --user app.guest:1234 --header 'Content-Type:application/json' --data '{"description":"Great App!"}' "https://instance.service-now.com/u_feedback.do?JSONv2&sysparm_action=insert"
{"records":[{"u_description":"Great App!","sys_id":"eeaa2f394f5c9a00dd83afdd0210c716" ... }]}

Disclaimer about polluting global namespace


You don't like the global variable com_acme_feedback_app_insert_sys_id, do you. Even with the hacky name-spacing going on? Okay.

Well, it just so happens SnowLib provides a convenient place to stuff these things. So, if you have any of the libraries in your instance, you could instead write to a 'global' variable thusly:
function onBefore(current, previous) {
  SnowLib.feedback_app_insert_sys_id =
    current.sys_id.toString();
}
Of course, now you are just polluting SnowLib. But maybe in the future, SnowLib will contain a nicer facility: a server-side scratchpad, if you will. Until then, the above should work just fine.

No comments:

Post a Comment