AnsweredAssumed Answered

Account Role Detection

Question asked by cesbrandt on May 16, 2016
Latest reply on May 19, 2016 by cesbrandt

Edit (5/20/2016): Below is the code I came up with to build a global array of all Account Roles assigned to the ENV['current_user_id'] relative to the page being accessed.

 

Rewriting this edit has turned it more into a Guided Practice than a simple posting of the code. Not really my style (I certainly don't write the material we teach, so my apologies if it's bad), but I hope it helps to give everyone ideas on how they could use this.

 

/**
 * Executes an XMLHttpRequest and the supplied callback, providing the
 *    responseText and GET URL as variables of the callback function.
 *
 * Important Note: This function assumes the results of the XMLHttpRequest will
 *    be JSON data prepended by "while(1);".
 */
function getJSON(apiURL, callback) {
 xhrRequests++;
 var xhr = new XMLHttpRequest();
 xhr.open('GET', apiURL, true);

 xhr.onload = function() {
  var data;
  if(xhr.status >= 200 && xhr.status < 400) {
   try {
    callback(JSON.parse(xhr.responseText.replace('while(1);', '')), apiURL);
    xhrRequests--;
   } catch(e) {
    console.log(e);
   }
  } else {
   console.log('API Access Error ' + xhr.status + ': ' + xhr.statusText);
  }
 };

 xhr.onerror = function(e) {
  console.log('API Access Error: ' + e.error);
 };

 xhr.send();
}

/**
 * Identifies the parent account of a course or sub-account and retrieves the
 *    Account Roles for that account.
 */
function getParentAccount(id, type) {
 /* Types
    0 = Accounts
    1 = Courses
  */
 var apiURL = '/api/v1/' + ((type === 0) ? 'accounts' : 'courses') + '/' + id, success;
 if(type === 0) {
  success = function(data) {
   var accountID = data['parent_account_id'];
   if(accountID != null) {
    getAdminList(accountID);
   }
  };
 } else {
  success = function(data) {
   getAdminList(data['account_id']);
  };
 }
 getJSON(apiURL, success);
}

/**
 * Retrieves the Account Roles for the current user for the designated accountID
 *    and pushes them to the userRoles global array.
 */
function getAdminList(accountID) {
 getJSON('/api/v1/accounts/' + accountID + '/admins?user_id=' + ENV['current_user_id'], function(data, apiURL) {
  data.forEach(function(obj) {
   userRoles.push(obj);
  });
  getParentAccount(apiURL.split('/')[4], 0);
 });
}

/**
 * Triggers a series of XMLHttpRequests to build a single list of all account
 *    role assignments for the current user that applies to the current page
 *    being accessed.
 *
 * Important Note: Additional logic restricts use fo this function to accounts
 *    and courses pages
 */
function adminRoles() {
 window.url = window.location.href;
 window.rolesRetrieved = false;
 var type = (url.indexOf('/courses/') != -1) ? 1 : ((url.indexOf('/accounts/') != -1) ? 0 : null);

 if(type !== null && ENV['current_user_roles'].includes('admin')) { // If type of page is course or account
  var id = url.split('/')[url.split('/').indexOf((type === 1) ? 'courses' : 'accounts') + 1];

  // Generate global variables for storing the consolidated list of roles and to
  //    track the active XMLHttpRequests
  window.userRoles = [], window.xhrRequests = 0;
  if(type === 1) {
   getParentAccount(id, type);
  } else {
   getAdminList(id);
  }

  var wait = setInterval(function() {
   if(xhrRequests <= 0) {
    rolesRetrieved = true;
    clearInterval(wait);
   }
  }, 500);
  return true;
 }
 console.log('Invalid page for adminRoles() execution.');
 return false;
}

 

Important Note: This is pure JavaScript, no jQuery, and is fairly heavy on validation. Specifically, this is only meant to work with the accounts and courses pages. The restriction is primarily due to my not having had a chance to look into the API for profile, calendar, and inbox pages. I may revisit this at a later time, but that is doubtful. Nearly every feasible "custom" functionality I can think of that would not be globally applied would belong to a courses page.

 

The entire process is triggered by calling adminRoles(). It contains validation for execution from courses or accounts pages and to only continue execution if ENV['current_user_roles'] contains admin. If the current page is valid and the current user has an Account Role, the function will return true, otherwise it'll return false.

 

As the XMLHttpRequests are executed, the Account Roles assigned to the ENV['current_user_id'] relative to the current page will be appended to a global array (userRoles) for manipulation after all the roles have been identified.

 

In addition, there is also a global variable (rolesRetrieved) created to track whether the list of assigned Account Roles has finished being generated. Since the XMLHttpRequests are asynchronous, a timer needs to be used to watch this variable and wait until it is set to true before trying to work with the roles.

 

As such, the most simplistic way to call this function would be:

 

window.onload = (function() {
 /* BEGIN ACCOUNT ROLE LIST GENERATION */
 if(adminRoles()) {
  var wait = setInterval(function() {
   if(rolesRetrieved) {
    // Insert Code to Handle the generated list
    clearInterval(wait);
   }
  }, 500);
 }
 /* END ACCOUNT ROLE LIST GENERATION */
})();

 

This will execute adminRoles() on EVERY page for EVERY user. However, the validation at the very beginning of the function will prevent any of the time-consuming scripting from executing for users that do not have an Account Role or that are on a page where the parent_account_id cannot be traced.

 

Simple validation of the current page can be used to identify whether the function should execute. Any method can be used to identify whether the script should continue. A particular element ID or class name could be used on the pages where it should be used, validating the URL, or a combination of these and other means. How a page is validated to execute the script is entirely dependent upon the needs of the institution.

 

It can also be used to specify different code segments that can be executed by Account Role and be page-specific. However, how this gets implemented is entirely up to the developers. There are a few obvious options, each with their benefits and problems.

 

In terms of prevent unwanted execution on pages where the generated list would not be used, you can wrap the entire Account Role List Generation in another IF statement that validates the page for your requirements.

 

window.onload = (function() {
 // Only generate the Account Role List on a courses' Modules page (ensuring module items are not included)
 if(window.location.href.indexOf('/courses/') != -1 && window.location.href.indexOf('/modules') != -1 && window.location.href.indexOf('/modules/') == -1) {
  /* BEGIN ACCOUNT ROLE LIST GENERATION */
  if(adminRoles()) {
   var wait = setInterval(function() {
    if(rolesRetrieved) {
     // Insert Code to Handle the generated list
     clearInterval(wait);
    }
   }, 500);
  }
  /* END ACCOUNT ROLE LIST GENERATION */
 }
})();

 

As shown in this example, the only place where adminRoles() will execute is a page with a URL containing "/courses/" AND "/modules" BUT NOT "/modules/". The intent being to allow the function to run on the Modules page without it running on module items, like URLs.

 

In terms of execution and load times, this style of implementation is ideal as it will prevent any of the script from being executed if the current page has no need for it. However, it also can get difficult to manage this for use on multiple, specific page types. Following the same route, adding functionality that is generalized would also need to be specified. Thee page restrictions would need to be identified in the outer IF statement which would prevent unwanted execution of adminRoles() and in the IF statement that executes the Account Role-specific scripting.

 

window.onload = (function() {
 // Only generate the Account Role List on a courses' Modules page (ensuring module items are not included)
 if((window.location.href.indexOf('/courses/') != -1 && window.location.href.indexOf('/modules') != -1 && window.location.href.indexOf('/modules/') == -1) || window.location.href.indexOf('/accounts/') != -1) {
  /* BEGIN ACCOUNT ROLE LIST GENERATION */
  if(adminRoles()) {
   var wait = setInterval(function() {
    if(rolesRetrieved) {
     // Insert Code to Handle the generated list for general functionality on all pages
     if(url.indexOf('/courses/') != -1 && url.indexOf('/modules') != -1 && url.indexOf('/modules/') == -1) {
      // Insert Code to Handle the generated list for functionality specific to the Modules page of courses
     }
     if(url.indexOf('/accounts/') != -1) {
      // Insert Code to Handle the generated list for general functionality on all Accounts pages
     }
     clearInterval(wait);
    }
   }, 500);
  }
  /* END ACCOUNT ROLE LIST GENERATION */
 }
})();

 

Alternatively, the function could be ran on EVERY page and then additional functionality dictated only afterward the list has been generated. The overall code would be the same, but the pages regulation would only be handled in one location, making it easier to manage.

 

Lastly, I recommend using the role_id for matching roles to functions. However, since a user can be assigned a role at multiple levels, we don't want to execute the code twice. To prevent this, I would recommend using a "is it set" object to track whether a role's portion of the script has already been executed.

 

window.onload = (function() {
 // Only generate the Account Role List on a courses' Modules page (ensuring module items are not included)
 if((window.location.href.indexOf('/courses/') != -1 && window.location.href.indexOf('/modules') != -1 && window.location.href.indexOf('/modules/') == -1) || window.location.href.indexOf('/accounts/') != -1) {
  /* BEGIN ACCOUNT ROLE LIST GENERATION */
  if(adminRoles()) {
   var wait = setInterval(function() {
    if(rolesRetrieved) {
     var set = {};
     if(typeof set['generic'][0] === 'undefined') {
      // Insert Code to Handle the generated list for general functionality on all pages
     }
     switch(role['role_id']) {
      case 1:
       if(typeof set['generic'] === 'undefined') {
        set['generic'] = [];
       }
       if(typeof set['generic'][role['role_id']] === 'undefined') {
        // Insert Code to Handle the generated list for general functionality on all pages for role 1
       }
       break;
      case 3:
       if(typeof set['generic'] === 'undefined') {
        set['generic'] = [];
       }
       if(typeof set['generic'][role['role_id']] === 'undefined') {
        // Insert Code to Handle the generated list for general functionality on all pages for role 3
       }
       break;
     }
     if(url.indexOf('/courses/') != -1 && url.indexOf('/modules') != -1 && url.indexOf('/modules/') == -1) {
      if(typeof set[0] === 'undefined') {
       set[0] = [];
      }
      if(typeof set[0][0] === 'undefined') {
       // Insert Code to Handle the generated list for general functionality on all pages
      }
      switch(role['role_id']) {
       case 1:
        if(typeof set[0][role['role_id']] === 'undefined') {
         // Insert Code to Handle the generated list for functionality specific to the Modules page of courses for role 1
        }
        break;
       case 5:
        if(typeof set[0][role['role_id']] === 'undefined') {
         // Insert Code to Handle the generated list for functionality specific to the Modules page of courses for role 5
        }
        break;
      }
     }
     if(url.indexOf('/accounts/') != -1) {
      if(typeof set[1] === 'undefined') {
       set[1] = [];
      }
      if(typeof set[1][0] === 'undefined') {
       // Insert Code to Handle the generated list for general functionality on all pages
      }
      switch(role['role_id']) {
       case 2:
        if(typeof set[1][role['role_id']] === 'undefined') {
         // Insert Code to Handle the generated list for general functionality on all Accounts pages for role 2
        }
        break;
       case 7:
        if(typeof set[1][role['role_id']] === 'undefined') {
         // Insert Code to Handle the generated list for general functionality on all Accounts pages for role 7
        }
        break;
      }
     }
     clearInterval(wait);
    }
   }, 500);
  }
  /* END ACCOUNT ROLE LIST GENERATION */
 }
})();

 

Tweaking the structure and validation restrictions would be entirely dependent upon the needs of the institution, but hopefully I've given good, basic, ideas on how this can be used.

 

 

 

Original Post

Here's the basic situation: We have customized features (i.e. link homepage generators and inline editing icons) meant to help keep our courses uniform and ease some of the functionality of Canvas. However, these features are not intended for display to students (though they do no harm).

 

Currently, we're restricting access via Canvas user IDs as retrieved from the ENV constant. Originally, we'd considered restricting them via ENV['current_user_roles'], but found that, for some reason, students receive the teacher role, making it unreliable. Here's our current implementation:

 

// Array of MCDAs
var mcda = [
  657,
  658
];

// On Window Load
window.onload = (function() {
  if((' ' + document.body.className + ' ').match(/modules/) && mcda.indexOf(parseInt(ENV['current_user_id'])) != -1) {
    ...
  }
})();

 

This method is not ideal since the JavaScript file will need to be updated whenever the access list is updated. However, there is a select group on individuals that utilize these "special" features and they are all members of the same account roles. However, the ENV constant offers nothing with regards to identifying the account roles of the current user.

 

Is it possible to use JavaScript to detect what account role (if any) a user has when they access a particular page?

Outcomes