Database and Workflow Security (Locking Records/Actions Based on any Criteria)

Database and Workflow Security Guide

The purpose of this guide is to show you how to lock down your records and tighten security. One issue that I have experienced is that users who bookmarked a page they shouldn't be able to go back to, or pressed the Back Button in their browser when they shouldn't have, or who know the URL to a Form and the Record ID of a record they want to modify... can manipulate data. Knack, by default, only has one level of security which is restricting page access with Login roles and that's not enough. This guide provides the following solutions:

  1. How to prevent specific, or all, actions based on status. (Ex: If "Status" is "Closed" then prevent all edit actions)
  2. How to assign a record to user(s) and prevent all other users from editing it.
  3. How to create fully customized security based on literally any criteria.

This guide starts by showing some basic tactics (non-Javascript solutions) we can use from the Builder's user interface and then goes further beyond into our more robust Javascript solution which allows us to lock records based on any criteria.

Basic Workflow Security in Builder (Using Page Rule - Redirect):

Page Rules are great for securing pages based on the status of a record. For this example, I have a workflow which moves records down a pipeline of Phases and Statuses (think of Phase like where we are in a process and Status as the action being taken). The Phases go from "Unassigned" all the way to "Closed". When a record is in the "Unassigned" Phase then a Manager will be presented with an action to assign that record to a Employee. To assign a record, I have a menu button (or action link) appear which redirects the user to another page and is restricted to the Manager user role.

The only time a record should be assigned is when it is in the "Unassigned" status. So, on that page, here's an example Page Rule I could use to prevent Managers from accessing this form when the status is not "Unassigned":

  ![](upload://wbSKoJ46MbgLI9sAfjb0dHtsGRi.png)

That's the most basic example using only one criteria. When the Status is not Unassigned then the Manager will be redirected to the parent page and prevented from taking action. Often times we will have multiple page rules set up to prevent action. Create an individual page rule per your criteria. For example, I might add something like "when Assigned To is not blank" if I have a seperate form for the Reassign Task process. 

Javascript Security (Restricting Records Based on Logged In User and Beyond)

Now let's consider something more complex. We have already assigned a record to an Employee. Let's say my database keeps track of who the record is "Assigned To" and who it was "Assigned By". I only want the "Assigned To" user and the "Assigned By" user to be able to perform a certain action such as: changing the "Due Date" for the record. However we cannot enforce that using Page Rules because Page Rules don't allow us to check if a data-field is the current logged-in-user. We must now resort to Javascript (for more basic checks, use the Page Rule solution above).

Builder Setup for Javascript Solution

To make our code work, you need to create a "Details" view that contains the data you need to work with. So, for my example, I have a "View Details" view for my assigned tasks and this "Details" view would go inside of that page. On that Details view, add all the data fields you want to provide custom validation for.

Javascript Code

Note: In this code, you will need to know the "field_xxx" number for the fields you want to validatie (in this example I use "Assigned To" and "Status"). To find: go to your Builder. Select "Data". Go to your "Object" and click the "Settings" icon next to the Field. Check the URL for your Field ID.

Copy/Paste this code into your Javascript. And fill in the correct view numbers / data fields.

// Hidden View for Ajax Request is Removed from DOM before Rendering
$(document).on('knack-view-render.view_2740', function(event, view, record) {
$("#" + view.key).remove();
}):

// Ajax GET requests gets the data we need to validate our custom security
// Success function passes data to custom_Validation
// Error function tries to get the data again otherwise kicks user back to previous page
function myAjaxTest(recordID, formView, boolFormSubmitted) {
$.ajax({
url: "https://api.knack.com/v1/pages/scene_895/views/view_2740/records/" + recordID,
type: "GET",
headers: {
"X-Knack-Application-Id": Knack.application_id,
"X-Knack-REST-API-Key": `knack`,
"Authorization": Knack.getUserToken()
},
tryCount: 0,
retryLimit: 3,
async: false,
success: function(data) {
custom_Validation(data, recordID, formView, boolFormSubmitted);
},
error : function(XMLHttpRequest, textStatus, errorThrown ) {
this.tryCount++;
let tryCount = this.tryCount, retryLimit = this.retryLimit, seconds;
if (tryCount <= retryLimit) { //try again
switch(tryCount) {
case 1:
case 2: seconds = 5; break;
case 3: seconds = 10; break; }
let timeout = seconds * 1000;
console.log("Error: " + XMLHttpRequest.status + " " + XMLHttpRequest.statusText + "\nRetry Count: " + tryCount + "\nRetrying in " + seconds + " seconds")
let ajaxObject = this;
window.setTimeout(function(){
$.ajax(ajaxObject);
}, timeout);
} else {
const indention = "\n";
alert("Server Error:" + indention + "Refreshing page due to load error." + indention + "Notify your site administrator if issue persists.");
window.location.reload();
}
}
});
}

// This is an example custom validation function
// You can get whatever data you want and run whatever tests you want as well
function custom_Validation(data, recordID, formView, boolFormSubmitted) {
// In this example, I check for two things:
// 1: Is the logged in user who it is assigned to?
// 2: Is the record NOT closed?
// If either are false then the user is redirected to another page
const logged_In_User = Knack.getUserAttributes().name,
// find the data below in your builder
assignedTo = data.field_205_raw,
status = data.field_192;

// This is just an example function if validation fails
function failed_Validation(custom_Text, redirect_URL) {
window.alert("Action Prohibited:\n" + custom_Text);
window.location.replace(redirect_URL); // This is a URL to redirect to. Can be window.history.back()
}

let Employee;
if (assignedTo && assignedTo.length > 0) Employee = assignedTo[0].identifier; // user data fields are objects and this is the user's name

// Here is the actual test, if it fails then the failed_Validation routine fires
if (status == "Closed" || logged_In_User !== Employee) {
failed_Validation("This request is either assigned to another user or closed.", "https://yoursite.knack.com/site#error/")
}

// This checks if a form was being submitted during this routine, if so then the submission continues
if (boolFormSubmitted == false) {
return;
} else {
const $form = $('#' + formView);
$form.find('button[type=submit]').submit();
}

}

// A Form You Want Custom Valdiation On
$(document).on('knack-view-render.view_800', function(event, view, record) {
Knack.showSpinner();
const recordID = record.id,
formView = view.key;
let boolFormSubmitted = false;

// This is an example that runs the validation tests when view renders
myAjaxTest(recordID, formView, boolFormSubmitted);

// This is an example that runs the validation tests when form is submitted
$("#" + formView + " .kn-button").on("click", function(event) {
event.preventDefault();
boolFormSubmitted = true;
myAjaxTest(recordID, formView, boolFormSubmitted);
});

});

Expanding This Code to Meet Your Needs

This code snippet is just a foundation. If you want to perform checks on any other aspect of your data then you can do that. The ".remove()" function is also versatile in that you can delete views from the HTML on page-load that you don't want specific users to see or have access to.

1. Create two fields: "Created by" and "Edited by".

2. Created another field called "[Field] Edited". For example: 

![](upload://2h0EW3uiQTJ3jCZxrCPJNfqZaxq.png)

So your final object looks like this:

![](upload://pCW31er92zIHsOHYfnY2COFWJfv.png)

 

Then create a conditional rule on your field. For example "Title" would have a conditional rule:

![](upload://2HfeVcnB0RbFS4SMxUnjn7Qg8y7.png)

 

How this works:

Users are never editing the "Title" field but the "Title: Edited" field. When it's edited by the person who created it then we use it, otherwise we don't. 

To implement this in your app, your Edit Record Form should use the "Title: Edited" instead of "Title" field. The Add Record form should have the "Title" field.  When someone adds or edits a record, we set "Created by" and "Edited by" to the logged-in user. 

 

Optional:

You can run this task:

![](upload://acbpQMAj0lGLB8w1BWCQD8faTD4.png)

This will cleanup any "Edit" fields (e.g. "Title: Edit") that were edited by unauthorized users. This is completely optional but a better user experience IMO, otherwise the Edit Form will potentially contain random text in it entered by the last person who tried to edit it. (Another option is to use sessionStorage to populate the "Edit" field on view render. For example, when a user visits the Edit Form, I populate the "Title: Edit" field with the value of the "Title" field on the previous page. This is what I do in my app).

Great question, I'm not sure if we can block API requests made from outside of Knack on client side. That may be something Knack needs to do from their server. I'm curious if there is a solution for that?

In the mean time, I just overhauled this post to reflect some updates on the code I'm currently using.