I had to solve a similar problem in an application that I currently developed, but permissions are based on user country.
I know you already are, but just to reiterate for the benefit of anyone reading, permissions should always be implemented on the server side and JavaScript security is always secondary. This is because anyone with half a brain can use either a bookmarklet or Firebug to execute arbitrary JavaScript and circumvent your client-side security.
I've found that there are several ways to do this, but there are two approaches in particular that are the most sane. Regardless of the approach, there are two things to consider: 1) what JavaScript to server and how to avoid serving unnecessary logic, and 2) how to avoid executing logic that is not available to the user.
Lazy loading and permissions configuration:
All widgets in my application are lazy loaded via dojo.require
, so there was no need to worry about the unnecessary inclusion of JavaScript that wasn't applicable to the user. I am not deeply familiar with the ExtJs library, but as far as I have seen, it doesn't supply a comparable method; however, it is essential a synchronous ajax call followed by an eval. In any case, in this method it is important that component functionality doesn't cross-cut through different files (also, this is generally good design). Each file should be its own class that controls a specific widget or other UI element.
I then set permissions in a server generated JavaScript config class. This class can then be referenced throughout the application for what is and what is not allowed.
For example, the following method controlled access to most of the widgets that were available from global controls.
com.project.frontController.prototype.init = function(widgets) {
var permissions = com.project.config.permissions;
// For each widget in the controller
for ( var i=0, l=widgets.length; i<l; ++i ) {
// If access is restricted to the widget (by id), then disable it.
if ( !permissions[ widgets[i].id ] {
com.project.util.disable(widgets[i].btnAccessNode);
}
// Otherwise, leave it enabled and connect the necessary event handlers.
else {
// connect an onclick handler or whatever is required for the widget
}
}
};
And the config looked something like:
com.project.config.permissions = {
"widgetAbc": {
btnAccessNode: "#some-css-selector",
otherWidgetConfig: "etc"
},
"widgetXyz": {
btnAccessNode: "div.some-css-selector"
}
};
Compile and functionality check:
Some applications will compile all of its JavaScript into one file, and then serve it to the client. If your application does such a thing, and you can manage to do this dynamically, all necessary JS can be determined on the server-side before it is served. Of course, this will accrue some overhead; however, you can cache compiled versions by role. If the roles are few, then this cache can be easily primed and it's just a matter of including one specific script.
Since only the allowed JavaScript will be available, simply detecting if the required class/function is available before attempting to execute it is all you need to do for your permissions check.
For example, you script inclusion may look like this:
<script type="text/javascript" src="/js/compiler.php?role=moderator"></script>
And your functionality check would be something like this:
com.project.frontController.prototype.init = function(widgets) {
// For each widget in the controller
for ( var i=0, l=widgets.length; i<l; ++i ) {
// If the widget controller doesn't exist, disable it.
if ( !com.project.util.widgetExists[ widgets[i].id ] {
com.project.util.disable(widgets[i].btnAccessNode);
}
// Otherwise, leave it enabled and connect the necessary event handlers.
else {
// connect an onclick handler or whatever is required for the widget
}
}
};
or
com.project.someWidget.prototype.launchOtherWidget = function() {
if ( typeof otherWidget != "undefined" ) {
(new otherWidget()).open();
}
};
Notice that both methods are very similar in implementation. I would say the best way to make a determination between the two is to consider the size of your codebase, the tools available to you for compiling on the fly, and caching those role-based, compiled packages. For my project, not only was a compiler not available in the environment, but the code base was large (1.3mb inflated/296kb defaulted plus the dojo library), which wasn't acceptable as the client was more interested in maintaining low application load times.