Jenkins Plugin Tip: Access control and visibility in actions

As I discussed before, Action is one of the primary ways plugins use to add more information to the top page, project pages, build pages, and so on.

Today I’d like to talk about writing protected actions that require some kind of access control. Depending on your requirements, there are several ways to do this. There are three parts in Action that affects behaviours with this regard.

One is the getIconFileName() method. As its javadoc states, returning null from this method hides your action from the HTML page. So if you have an action and you don’t want to show it for users who don’t have the permission to access it, it could be something like the following:


public String getIconFileName() {
    return Jenkins.getInstance().hasPermission(Model.LIST)
         ? "gear.png" : null;
}

The next is the getUrlName() method. This method also allows null as the return value. This might sound similar to getIconFileName() but the implication is different. With getIconFileName() returning null, the rendered HTML page won’t show the link but if someone knows the URL, they can still access it. But with this method returning null, it’s as if no such URL is recognized.

So with the code like the following, if someone without the permission requests it, he’ll get 404 not found (modulo a bug, which I just fixed toward 1.443). This is sometimes desirable, as not only can you hide the data, but you can also hide the fact that something exists. For jobs, Jenkins does this — http://jenkins/job/top-secret-project/ returns 404 not found, not 401 forbidden, so guessing the project name will not help attackers gain any information.


public String getUrlName() {
    return Jenkins.getInstance().hasPermission(Model.LIST)
         ? "template" : null;
}

But this is sometimes undesirable, as users will not be prompted for authentication. If someone hits the link with an expired session, he’ll get 404 not found and he needs to be smart enough to know that this is because he hasn’t logged in, then navigate manually to the login page and come back. To avoid this problem, you’ll do the following and advertise the URL all the time.


public String getUrlName() {
    return "template";
}

And that brings me to the third part, because getUrlName() now always returning non-null means you aren’t actually checking if those who are accessing has a permission to do so. To do this, you use StaplerProxy.


class MyAction implements Action, StaplerProxy {
    ...
    
    public Object getTarget() {
        Jenkins.getInstance().checkPermission(Model.LIST);
        return this;
    }
}

What happens is that when the URL that someone requests hits this action (or something underneath), we’ll verify that the requestor has the permission. If not, this will initiate the authentication, such as redirecting the user to the login page, or initiating the OpenID protocol with the preconfigured identity provider. Normally this interface is used to defer the UI processing to another object (sort of like how symlink works), but in this case we return this to indicate that we process this request by ourselves, and it’s smart enough not to cause infinitely recursion.

I guess the rule of thumb is that (1) you have to check the permission either in getUrlName() or getTarget(), or else there’s no access control, (2) you use getIconFileName() control the visibility in the HTML page, and (3) you use getUrlName() to control if you want 401 or 404 in case the access is denied.

comments powered by Disqus