This article applies to version 1 of the Zendesk Apps framework. A newer way of building apps is available. For more information see Zendesk App Framework v2.

Hello again! In the first part of this diary, I walked through how to build an app that did the following:

  • When the agent clicks a big red button, the app adds a tag, displays a notification, and changes the assigned group on the ticket.
  • The agent can collapse the app window by clicking an icon.

For details, see Developer diary: Building a Zendesk App (Part 1) .

What if you need to restrict access to the app by group or by a list of groups?

The obvious alternative is to leverage the built-in role restrictions, which lets you restrict the app to either agents or admins (or other roles if using custom roles on the Enterprise plan).

I'm going to update my app so that it displays an "Access denied" message if the agent viewing the ticket belongs to a group that's not authorized to access the app.

If you want to preview the finished project, the full code is attached at the end of this document.

January 20, 2014

If you're certain that the restricted group or groups will never change -- ever -- then you could hard-code the group ID values into the function that displays the "Access Denied" message.

Given that someone might need to change the groups that are restricted, it's a good idea to use an app setting instead of hard-coding the groups. An app setting lets an admin change the group IDs anytime without changing the app's source code.

App settings are defined in the manifest.json file as parameters. The Apps framework uses the parameters to automatically generate a Settings page with form fields for each parameter. The Settings page is displayed when an admin installs the app or selects the Change Settings option on the Manage Apps page in Zendesk Support.

The first code snippet below shows the contents of my manifest.json file before I add any app settings:

{    "name": "Reset Assignee",    "author": {        "name": "Jeremiah Currier",        "email": "[email protected]"    },    "defaultLocale": "en",    "private": true,    "location": "ticket_sidebar",    "version": "1.0",    "frameworkVersion": "0.5"}

After adding an app setting, my manifest file looks like this:

{    "name": "Reset Assignee",    "author": {        "name": "Jeremiah Currier",        "email": "[email protected]"    },    "defaultLocale": "en",    "private": true,    "location": "ticket_sidebar",    "version": "1.0",    "frameworkVersion": "0.5",    "parameters": [        {            "name" : "hideForThisGroup",            "type" : "text",            "required" : false        }    ]}

The differences between the two versions are the comma after "frameworkVersion": "0.5" , and the new parameters list. The hideForThisGroup parameter lets an admin specify the restricted groups using the app's Settings page.

As a side note, here's what the parameters list might look like if you defined more than one parameter:

"parameters": [    {        "name" : "hideForThisGroup",        "type" : "text",        "required" : false    },
    {        "name" : "parameterTwo",        "type" : "text",        "required" : false    }]

Now my app has an optional parameter. It's optional because "required": false . I'll cover how to use the parameter next.

January 23, 2014

After somebody specifies the restricted group or groups on the Settings page of the app, the information is available for use in the app. For instance, the app can use the info to switch to a template that displays "Access Denied" if the current user is a member of any group that's not authorized to access the app.

What I need now is a function that gets the groups the current user belongs to. It'll use the data to see if any of the groups matches the restricted groups the admin specified for the hideForThisGroup parameter. If there's one or more matches, the function will display the "Access Denied" message.

I'll call my function checkCurrentAgentsGroups . I want the function to run as soon as the app finishes loading. Accordingly, I'll get my function to listen for the app.activated event. The app.activated event fires when the app is loaded. For more information, see Lifecycle in the documentation.

I add the following statement in the " events " section of my app.js file to run my function when the app finishes loading:

events: {    'app.activated' : 'checkCurrentAgentsGroups'},

I'll define the checkCurrentAgentsGroups function elsewhere in the app.js file. It'll look something like this:

checkCurrentAgentsGroups: function() {    // code here}

To be continued.

January 27, 2014

Time to work out the details of the checkCurrentAgentsGroups function. The function needs to do the following things:

  • Obtain the group IDs for the current user
  • Obtain the group IDs of restricted groups, as specified by my app setting
  • Compare these values
  • Either provide or deny access to the app based on the comparison

That's a lot. A skeleton for the function might look like this:

checkCurrentAgentsGroups: function() {
    // var variable1 = array of group IDs for the current user    // var variable2 = the group ID values from the app setting
    if ( conditional statement here ) {        // switch to the template for the authorized user    } else {        // switch to the template for the unauthorized user    }
}

Let's start with "variable1". How can an app get an array of group IDs for the current user? A great place to start is to check the Current User API doc . The doc says the this.currentUser() method returns the currently authenticated user as a user object . The user object in turn has a user.groups() method that returns the user groups as an array. Essentially, you call methods of the user object to get different information about the user.

The doc provides me with enough information to write the following statement to get the group IDs of the current user and assign them to a variable:

var groupObjects = this.currentUser().groups();

I changed the variable name from "variable1" to "groupObjects" to be more descriptive.

Time to test the code. I open up my browser's developer tools in Chrome: View > Developer > Developer Tools. (For Firefox: Tools > Web Developer > Web Console.) I make sure the ZAT server is running and add a console.log(groupObjects); statement under the variable I just defined. The JavaScript console.log() function displays -- or logs -- the value of its arguments in the browser console.

var groupObjects = this.currentUser().groups();console.log(groupObjects)

I reload the page and check to browser console:

Wait, those aren't group IDs. They're group objects. The strange results displayed in the console are due to the way the Apps framework packages data.

Here's a simplistic example of an array of group objects:

[ { OBJECT ONE } , { OBJECT TWO } , { OBJECT THREE } ]

From the console, it's clear that what's returned is not very friendly to work with. I'll need to manipulate the data to extract the group ID numbers. I'll have to save that until next time.

February 1, 2014

The problem I'm trying to solve today is extracting group ID numbers from an array of group objects.

The groups() method of the user object returns an array of group objects. Looking at the group object doc , I know that calling the id() and name() methods on each group object returns the group's ID and name.

There's more than one way to get data from an array of JavaScript objects. I'll use a function I found in a JavaScript library called "underscore.js". (For more on underscore.js, see their website .)

Specifically I'll use the underscore.js function called map() :

_.map(list, iterator, [context])

The map() function produces a new array of values by running (or mapping) each value in the list through a transformation function (iterator). For details, see the map function in the underscore.js docs.

It makes a little more sense if I replace the arguments with the values for my app:

var result = _.map(groupObjects, function(group){ return group.id(); });

The value of my list argument is groupObjects , and the value of my iterator argument is a function definition, function(group){ return group.id(); } .

Here's what's happening. The map() function takes the group objects in the list and passes them to the transformation function one object at a time. The transformation function calls the id() method of the group object. The map() function appends the returned value to the result array.

That's the theory. I'll need to test it. I add a couple of console.log statements to my code:

var groupObjects = this.currentUser().groups();console.log(groupObjects);
var result = _.map(groupObjects, function(group){ return group.id(); });console.log(result);

In my browser, I open the console and reload the app:

Confirmed. The map() function built an array that contains the group ID values of the currently authenticated user. Awesome! The next step has to wait until next time.

February 3, 2014

Situation report. My task is to compare the current user's group IDs to the group IDs of the restricted groups to determine whether or not to allow the user access to the app. I have the first group of IDs, the array I built last time called result .

The next step is to create an array containing the IDs of the restricted groups. This second set of IDs is specified by the admin when he or she completes the app setting I created on January 20. The name I gave the setting is hideForThisGroup .

Let's start by grabbing the value the admin entered for the setting:

var settings = this.setting('hideForThisGroup');

That's great but what's the data type of the value assigned to settings ? Is it an integer, a string, an array? Here's how to find out:

console.log(typeof settings);

The result is String. Even if the admin enters a bunch of numbers in the app setting, the value is saved as a string. Example: "123456" instead of 123456 .

We need an array of numbers to compare against the result array. A string of numbers won't work. First I need to break up the string into smaller number strings. To do that, I call JavaScript's split() method to split the string into smaller substrings at the comma character:

var settings = this.setting('hideForThisGroup').split(",");

Now each group ID in settings is a substring separated by a comma -- in other words, an array of substrings. The next step is to convert the substrings into integers and add them to a new array, which we'll tackle next time.

February 7, 2014

To convert substrings into integers and add them to a new array, I once again turn to the map() function in the underscore.js library. I need to transform each of my substrings as follows:

  1. Remove any whitespace left after splitting the initial setting string. The admin might have typed spaces between the commas and the IDs.
  2. Convert the trimmed substring to an integer.

Here's a statement that should do the trick:

var settingsMap = _.map( settings, function(hideThese){     return parseInt(hideThese.trim(), 0); } );

The map() function takes the substrings in the settings list and passes them to the transformation function one substring at a time. The JavaScript trim() method gets rid of any whitespace before and after the substring. The JavaScript parseInt() method converts the trimmed substring into an integer.

Note: The parseInt() method takes two arguments: the string you want to convert and a "radix" -- in this case, a 0 (zero). If you want to know more about the radix value, see the parseInt() documention on the Mozilla Developer Network.

February 9, 2014

What I have so far:

  • an array named result containing integer values that correspond to the group IDs of the current user
  • a second array named settingsMap containing integer values that correspond to the IDs of the restricted groups.

The next step is to compare the two arrays to look for matches. I decide to use another underscore.js function named intersection() for the job:

var contains = _.intersection(result, settingsMap);

The intersection() function compares the two arrays specified in the argument list and returns an array containing matching -- or intersecting -- values, if any. (For more details, see the intersection function in the underscore.js docs.)

If the contains array contains one or more items, it means there was a match. The current user belongs to a group that's not authorized to use the app.

Now that we can tell if the user is authorized or not, we can implement the "Access Denied" message using the switchTo() method of the Apps framework. I want the app to switch to the template with the "Access Denied" message if the contains array has one or more items.

Here's the conditional logic that'll do that:

if (contains.length > 0) {    this.switchTo('hide');} else {   this.switchTo('show');}

The hide and show arguments are template files. Here's the content of each file:

hide.hdbs

<div class="app-container"><p>ACCESS DENIED</p></div>

show.hdbs

<div class="app-container"><button class="btn" type="button"><img src="{{assetURL "red-small.png"}}" alt="logo" /></button></div>

For more information on switching and using templates, see the App interface section in the documentation.

I finished the function I set out to write to determine whether or not the user is authorized to access the app:

checkCurrentAgentsGroups: function() {
    // Get the IDs of the groups the current user belongs to    var groupObjects = this.currentUser().groups();    var result = _.map(groupObjects, function(group){ return group.id(); });
    // Get the IDs of the restricted groups    var settings = this.setting('hideForThisGroup').split(",");     var settingsMap = _.map( settings, function(hideThese){         return parseInt(hideThese.trim(), 0);     } );
    // Check for matches    var contains = _.intersection(result, settingsMap);
    // Take the appropriate action    if (contains.length > 0) {         this.switchTo('hide');    } else {        this.switchTo('show');    }}

To summarize, I extended what my app can do beyond what I covered in part 1. The added functionality includes the ability to hide the app from an agent that's in a group, and the ability to specify multiple groups in the app settings to prevent agents in those groups from accessing the app. The source code of my updated app is attached to this article.

I hope it was helpful and fun!