cancel
Showing results for 
Search instead for 
Did you mean: 
cesbrandt
Lamplighter

Account Role Detection

Jump to solution

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. Smiley Happy

/**

* 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?

1 Solution

Accepted Solutions

Hi cesbrandt​,

As I mentioned ENV.current_user_roles does not necessarily correlate to the roles in the system, which is what you are seeing (e.g. somebody that's a TA gets "teacher" in the array).  If it's absolutely critical that only someone with the enrollment of teacher is given these tools, then your best bet is to use the Enrollments API​.

Essentially, query the current enrollment (you can get the user ID and course ID from ENV) and look for the returned role or type.  Look at the filters listed in the API page, you will be able to get exactly what you want in the query and not have to iterate over the results.

/api/v1/courses/{current course}/enrollments?user_id={current user}&role=TeacherEnrollment

will return an array of TeacherEnrollment objects for that user in that course (1 for each section), so you don't even need to dig through the return data, just check if the array.length > 0 and you know they have a teacher enrollment.

View solution in original post

19 Replies
Stefanie
Community Team
Community Team

cesbrandt, as this question is very specific and highly technical in nature, I've shared it with the Canvas Admins​ and Canvas Developers​ groups to enhance its visibility among the experts in this area.

dwahl_sales
Instructure
Instructure

Hi cesbrandt​,

I'd like to know more about how the students are receiving the "teacher" role in ENV.current_user_roles.  If that's the case it's probably a bug.  Are you using custom roles for students that were based on the built-in teacher role?  Because (I believe) ENV.current_user_roles values are populated from the parent role.  Meaning, say, if you created a custom role called "counselor" and based it off of "teacher" the ENV should show "teacher" and not "counselor".

I have used ENV.current_user_roles to successfully show/hide things based off of the array values without issue.

I, honestly, have no idea why it has a teacher role. I know that the role assigned to students is original "Student" course role.

Here's a screenshot of a student account (absolutely no account-level permissions) accessing a course where they have the "Student" role and the corresponding console.log of my calling the ENV['current_user_roles']:

184281_pastedImage_0.png

Checking the Course Roles and the permissions assigned, the only permission not set to "Use Default" is Read SIS data which is still disabled but has also been locked to prevent accidental enabling in a sub-account. I double-checked each editable setting for the role to ensure it was set that way, and checked them both globally and at the sub-account, to ensure it wasn't changed at some point (they weren't).

Further, in the case of this test student, I've confirmed that the Enrollments for the account is limited to ​Courses, no ​Accounts​ were listed.

There's definitely something wrong there.  Here's a user who only has a student enrollment on the dashboard and in a course:

Screen Shot 2016-05-18 at 8.27.58 AM.png

Screen Shot 2016-05-18 at 8.28.40 AM.png

You can see that they only have user and student in the current_user_roles array.  At this point I would suggest talking to whoever created the test user or reaching out to support.

Alright, so I was able to do a thorough investigation of the user permissions. My process was to create a brand new account and systematically add/remove access to it following the exact access assignments for the account in my earlier screenshot. The answer appeared almost immediately.

Our usual test account had been enrolled in one course as a TA and all others as a Student. Removing the enrollment as a TA from another course resulted in the loss of the teacher role in the ENV constant. This, however, doesn't make any sense to me. Wouldn't the teacher role only appear in the course where the account is enrolled as a TA?

Also, my original question is still applicable. I appreciate that we took a deeper look at the odd role assignments, but there are still several roles we have that would award the admin and teacher roles in the ENV constant, most of which would not require access to the custom components I've mentioned. Being able to restrict is by the ​Account Roles​ would be ideal. Is it possible?

Hi cesbrandt​,

As I mentioned ENV.current_user_roles does not necessarily correlate to the roles in the system, which is what you are seeing (e.g. somebody that's a TA gets "teacher" in the array).  If it's absolutely critical that only someone with the enrollment of teacher is given these tools, then your best bet is to use the Enrollments API​.

Essentially, query the current enrollment (you can get the user ID and course ID from ENV) and look for the returned role or type.  Look at the filters listed in the API page, you will be able to get exactly what you want in the query and not have to iterate over the results.

/api/v1/courses/{current course}/enrollments?user_id={current user}&role=TeacherEnrollment

will return an array of TeacherEnrollment objects for that user in that course (1 for each section), so you don't even need to dig through the return data, just check if the array.length > 0 and you know they have a teacher enrollment.

View solution in original post

Alright, thank you. I was under the impression the ENV['current_user_roles'] was supposed to reflect the roles the user should have on any particular page, not all the generic roles they've been assigned anywhere on Canvas.

I've held off asking this because it didn't go anywhere in helping track down the original problem, but have you looked at the ENV.IS_STUDENT field ? It may not be there or it may be with a true/false value. I'm not sure if it would be useful in discriminating between teacher and student, but it might be an area to explore.

You can also fetch information about the user's enrollments to see what roles they have. That's an extra API call, but sometimes you gotta do what you gotta do.

As for the other, maybe this will help with the explanation: it's "current_user_roles" as in roles that the current user has (anywhere), not user_current_roles as in roles that the user currently has (within a context like a course).

ENV['IS_STUDENT'] returns as undefined, regardless of the user and role assignments. I've looked through the constant and couldn't find it.

As for the original question, I'm going to follow API query route. I'll query the account admins and then filter it down by the role ID(s) that I want to grant access to the features. It'll need to be recursive, making several queries to test each sub-account up to the top-level, but it should be fairly easy to actually implement.

Honestly, I was hoping to avoid API calls, but if that's what's needed, it's what is needed.

I'll post up my completed code after I've written and tested it.