Saturday, November 14, 2009

Authorization in the Netbeans Platform

Authentication and Authorization wasn't really thought off much when the designers of the Netbeans Platform were doing their magic. There is no "right" of doing this, though most people settle on doing their authentication in a ModuleInstaller (similar to OSGi's Activator).

Authorization is even worse off. Luckily all actions in the Platform are done via an Action extension of some sort and their "enabled" state is honored in all places. So what I did for authorization was to create to extensions of the actions I use, nl. NodeAction and CallableSystemAction. Then when I create my Actions I rather extend one of these 2 depending on it's purpose, and annotate it with javax.annotation.security.RolesAllowed, specifying which roles are allowed to execute the action.

To enforce this I finalize the action's "enabled" checks, and provide another method where necessary which the implementation must now place it's "enabled" logic in. This way I can prevent an action to be enabled unless the user is authorized. If a user were to execute custom logic (which would require a debugger or code injection) he could still execute them, but it would have no effect as the result would just be another access denied as the server's authorizations would prevent the final action from being executed. I also fetch the list of roles at authentication time from the server, so I'm equipped with everything necessary for authorization.

To do the actual check if the action is authorized I have a utility "Netbeans Service" which takes an Object as an argument. This method will see if the object's class has @RolesAllowed annotation, and verify if the authenticated subject/user has at least one role "required" by this annotation. Further the "Admin" role is exempt from all authorization checks.

As an example I listed all 4 classes. I didn't list the actual logic of fetching the list of roles, so it's up to you to populate the list with whatever method.
package com.blogspot.qbeukes.auth;

AuthService.java:
public interface AuthService
{
  /**
   * @return True if this is an authenticated context
   */
  boolean isAuthenticated();

  /**
   * @param object
   * @return True if the current subject is authorized to used the specified object
   */
  boolean isAuthorized(Object object);
}

AuthServiceImpl.java:
package com.blogspot.qbeukes.auth;

import java.util.Set;
import java.util.TreeSet;
import javax.annotation.security.RolesAllowed;
import javax.swing.Action;
import com.blogspot.qbeukes.logging.Log;
import org.openide.util.lookup.ServiceProvider;

@ServiceProvider(service=AuthService.class)
public class AuthServiceImpl implements AuthService
{
  private static final Log log = Log.getLog(AuthServiceImpl.class);

  private static final String ADMIN_ROLE = "Admin";

  private Set<String> roles;

  private Boolean admin;

  public AuthServiceImpl()
  {
  }

  @Override
  public boolean isAuthenticated()
  {
    // change to to check if the user is actually authenticated
    return true;
  }

  /**
   * Return the roles for this user, loading it on the first calls
   * @return Set of roles
   */
  protected Set<String> getSubjectRoles()
  {
    if (roles == null)
    {
      // fetch the list of roles here
      roles = new TreeSet<String>();
      roles.add("Some Role");
    }

    return roles;
  }

  /**
   * Checks if the current subject has the "Admin" role. Also caches the result
   * @return True if the subject has the "Admin" role
   */
  protected boolean isAdmin()
  {
    if (admin == null)
    {
      admin = getSubjectRoles().contains(ADMIN_ROLE);
    }

    return admin.booleanValue();
  }

  @Override
  public boolean isAuthorized(Object o)
  {
    if (!isAuthenticated())
    {
      return false;
    }

    if (isAdmin())
    {
      return true;
    }
   
    Class<?> clazz = o.getClass();

    // if the class doesn't have the RolesAllowed annotation we assume no roles
    // are allowed, and return false
    // this just protects us against "forgetting"
    // to allow all roles
    if (!clazz.isAnnotationPresent(RolesAllowed.class))
    {
      return false;
    }

    String[] rolesAllowed = clazz.getAnnotation(RolesAllowed.class).value();

    // empty roles list, no-one has access
    if (rolesAllowed == null || rolesAllowed.length == 0)
    {
      return false;
    }

    Set<String> subjectRoles = getSubjectRoles();
    boolean authorized = false;
    for (String roleName : rolesAllowed)
    {
      if (subjectRoles.contains(roleName))
      {
        authorized = true;
        break;
      }
    }

    return authorized;
  }
}

AuthorizedNodeAction.java:
package com.blogspot.qbeukes.actions;

import org.openide.nodes.Node;
import org.openide.util.Lookup;
import org.openide.util.actions.NodeAction;

public abstract class AuthorizedNodeAction extends NodeAction
{
  @Override
  protected final boolean enable(Node[] activatedNodes)
  {
    return Lookup.getDefault().lookup(AuthService.class).isAuthorized(this)
      && enableForNodes(activatedNodes);
  }

  /**
   * @param activatedNodes
   * @return True if the action should be enabled for the specified nodes.
   */
  protected abstract boolean enableForNodes(Node[] activatedNodes);
}

AuthorizedSystemAction.java:
package com.blogspot.qbeukes.actions;

import org.openide.util.Lookup;
import org.openide.util.actions.CallableSystemAction;

public abstract class AuthorizedSystemAction extends CallableSystemAction
{
  @Override
  public boolean isEnabled()
  {
    return super.isEnabled() &&
        Lookup.getDefault().lookup(AuthService.class).isAuthorized(this);
  }

  /* Not necessary for authorization. All my actions use this, so I collected it here */
  @Override
  protected boolean asynchronous()
  {
    return false;
  }
}

So from here you can use AuthorizedSystemAction and AuthorizedNodeAction like so:
@RolesAllowed({
  "Some Role",
  "Some Other Role"
})
public class SomeAction extends SystemAction {...}
OR
@RolesAllowed({
  "Some Role"
})
public final class SomeNodeAction extends AuthorizedNodeAction
{
  @Override
  protected boolean enableForNodes(Node[] activatedNodes)
  {
    // application level enabled checks
  }
 
  //...//
}

The above code will unfortunately not hide the actions, though they are disabled. It at least prevents the user from starting a wizard or opening the view/dialog. In our application we have a custom navigation system, so most of our actions are accessed through this panel. In this panel's UI generation I do a check to hide the actions, so it's not really a priority for us. We do, however, have some actions in the menus/toolbars, so I'm working on finding another way to do authorizations for them.

Beyond this, authorizations for other panels/views are done programmatically by querying the AuthService, so to simplify this I have a utility method which does the lookup, validate the presence of the service and then query the service. This changes:
Lookup.getDefault().lookup(AuthService.class).isAuthorized(...);
TO
AuthUtil.isAuthorized(...);

It also does a check if AuthService actually has an implementation, for the odd situation where the module might have failed to load properly. In such a case the application is gracefully closed accompanied by an error message. I guess I could allow the user to continue and faced with errors from the server, though for our app I think it's was a better choice to make the application completely unavailable and get fixed rather than the experience be interrupted with endless error messages, which would most probably be the case if this module didn't load properly.

No comments: