Role based UI with DukeScript

It’s a very common requirement to show or hide elements in a UI based on the users roles. Depending on the view technology this can be quite a challenge. The most convenient way to do this, is by declaratively marking the sections. With DukeScript it’s pretty simple. You can bind the visibility of an element to some property in the view model.

The problem is, that you need to introduce a lot of properties in your viewmodel. It would be much better, if you could simply add roles to a global user object and use this to control visibility:

<h2>Content</h2>
<div data-bind="authorize: 'admin'">
        <div id="vm1" >
            Totally secret stuff=<span data-bind="text: prop"></span>
        </div>
</div>
<div data-bind="authorize: 'reader'">
    <div data-bind="stopBinding: true">
        <div id="vm2" >
            Public=<span data-bind="text: prop"></span>
            <div data-bind="authorize: 'editor'">
                A bit less public=<span data-bind="text: prop2"></span>
            </div>
        </div>
    </div>
</div>

The user object can live in our root context and we can check if this user has the required roles:

@Model(className = "SecurityManager", targetId = "", properties = {
    @Property(name = "user", type = User.class)})
public final class SecurityBinding {

    @Model(className = "User",
            properties = {
                @Property(name = "name", type = String.class),
                @Property(name = "roles", type = String.class, array = true),
                @Property(name = "availableRoles", type = String.class, array = true)},
            targetId = "")
    public static class Uservmd {}
}

Now we need to create the “authorize”-binding. The tricky part is, that this needs to work in any binding context. If we bind several viewmodels to different parts of the page, each of them has their own root context. So we cannot rely on a common $root.

In this example I’m using document.getElementById(“security-manager”) to get hold of an element in the relevant root context. I get the User object from it and subscribe to changes in the users roles:

    @JavaScriptBody(args = {}, body = "ko.bindingHandlers.authorize = {\n"
            + "    init: function(element, valueAccessor, allBindings, viewModel, bindingContext) {\n"
            + "         var value = ko.unwrap(valueAccessor());\n"
            + "         var user = ko.contextFor(document.getElementById('security-manager')).$root.user();\n"
            + "         var roles = user.roles;\n"
            + "         if (roles().indexOf(value)==-1){\n"
            + "                 element.style.visibility='hidden';\n"
            + "             } else { element.style.visibility='visible'; }"
            + "         roles.subscribe(function(changes) {\n"
            + "             console.log('change in roles checking for role '+value);     "
            + "             if (roles().indexOf(value)==-1){\n"
            + "                 element.style.visibility='hidden';\n"
            + "             } else { element.style.visibility='visible'; }"
            + "         });\n"
            + "    },\n"
            + "};")
    public static native void init();

If the user has the right role, I show the element, otherwise it will be hidden. A pretty simple way to control access in a fine-grained way.

You can try it out yourself. I created a github project for you to play with:

https://github.com/dukescript/securitymanager