Featured Post

Applying Email Validation to a JavaFX TextField Using Binding

This example uses the same controller as in a previous post but adds a use case to support email validation.  A Commons Validator object is ...

Saturday, October 24, 2015

Access Control with JavaFX Node Properties

This example applies access control to a JavaFX application by marking UI elements -- Buttons, MenuItems, ToolBar Buttons -- with a command.  Those commands are restricted using a user data structure.  Once a user is selected, a recursive algorithm is applied to the Scene root Parent which scans for marked elements, applied the required access control (shown, disabled, hidden).


All source code for this post is available on GitHub.

The following video demonstrates the application.  There are 3 commands: A, B, and C.  Each command can be executed as a MenuItem, as a Button in the main screen, or as a Button on the ToolBar.

The user "Carl" cannot access any command except for Close which doesn't have access control applied.

The user "Jim" can access all three commands (A, B, and C) across all controls (MenuItem, Button, ToolBar).

The user "Ralph" can access command A, but neither B nor C.  B is disabled for Ralph.  C is hidden for Ralph.  These rules apply to all controls.



The UI is defined in SceneBuilder.  The hierarchy shows the controls used in the application: MenuItems, ToolBar Buttons, and Buttons added to a main VBox.

Scene Builder Definition
@FXML initialize()

These are the fields created in the .fxml added to a JavaFX controller MainViewController.  All of the following code snippets appear in MainViewController.java.

    @FXML MenuItem miA;
    @FXML MenuItem miB;
    @FXML MenuItem miC;

    @FXML Button tbA;
    @FXML Button tbB;
    @FXML Button tbC;

    @FXML Button btnA;
    @FXML Button btnB;
    @FXML Button btnC;

    @FXML ChoiceBox<UserSecurity> cbUser;

UserSecurity is a user data structure that carries an access control vector.  This will be presented later.

I've also defined 3 enums.

    enum AccessType { HIDE, DISABLE, SHOW }

    enum CommandType {A, B, C}

    enum NodePropertiesKeyType { commandType }

My MainViewController @FXML initialize() method begins by marking each of the UI controls with a CommandType.

    miA.getProperties().put(NodePropertiesKeyType.commandType, CommandType.A );
    tbA.getProperties().put(NodePropertiesKeyType.commandType, CommandType.A );
    btnA.getProperties().put(NodePropertiesKeyType.commandType, CommandType.A );

    miB.getProperties().put(NodePropertiesKeyType.commandType, CommandType.B );
    tbB.getProperties().put(NodePropertiesKeyType.commandType, CommandType.B );
    btnB.getProperties().put(NodePropertiesKeyType.commandType, CommandType.B );

    miC.getProperties().put(NodePropertiesKeyType.commandType, CommandType.C );
    tbC.getProperties().put(NodePropertiesKeyType.commandType, CommandType.C);
    btnC.getProperties().put(NodePropertiesKeyType.commandType, CommandType.C);

I follow this with a definition of the UserSecurity objects.  These are user records packed with access control information.

    Map<CommandType, AccessType> access1 = new LinkedHashMap<>();
    access1.put(CommandType.A, AccessType.HIDE);
    access1.put(CommandType.B, AccessType.HIDE);
    access1.put(CommandType.C, AccessType.HIDE);

    UserSecurity s1 = new UserSecurity("Carl (No Access)", access1 );

    Map<CommandType, AccessType> access2 = new LinkedHashMap<>();
    access2.put( CommandType.A, AccessType.SHOW );
    access2.put( CommandType.B, AccessType.HIDE );
    access2.put( CommandType.C, AccessType.DISABLE );

    UserSecurity s2 = new UserSecurity("Ralph (Some Access)", access2 );

    Map<CommandType, AccessType> access3 = new LinkedHashMap<>();
    access3.put( CommandType.A, AccessType.SHOW );
    access3.put(CommandType.B, AccessType.SHOW);
    access3.put(CommandType.C, AccessType.SHOW);
    
    UserSecurity s3 = new UserSecurity("Jim (All Access)", access3 );

The initialize() method ends with some code to handle the UserSecurity ChoiceBox.

    cbUser.setConverter(new StringConverter<UserSecurity>() {

        @Override
        public String toString(UserSecurity object) {
            return object.getName();
        }

        @Override
        public UserSecurity fromString(String string) {
            if (string == null) return null;
            if (string.equalsIgnoreCase("Carl (No Access)")) {
               return s1;
            }
            if (string.equalsIgnoreCase("Ralph (Some Access)")) {
               return s2;
            }
            if (string.equalsIgnoreCase("Jim (All Access)")) {
               return s3;
            }
            return null;
        }
    });

    cbUser.getItems().addAll(s1, s2, s3);
    cbUser.getSelectionModel().select(s1);
    cbUser.setOnAction((evt) -> applySecurity(cbUser.getScene()) );

The ChoiceBox.setOnAction() method is new to Java 8 update 60.  If you're running an earlier Java 8, say "u51", you can replace this with a ChangeListener.  This modification was made in what's in GitHub.

UserSecurity Class

UserSecurity is kept in MainViewController.java.  It has methods that are called during processing.

    class UserSecurity {

        private final String name;
        private final Map<CommandType, AccessType> access;

        public UserSecurity(String name, Map<CommandType, AccessType> access) {
            this.name = name;
            this.access = access;
        }

        public String getName() { return name; }

        public AccessType getAccess(CommandType command) {
            Objects.requireNonNull(access);
            AccessType a = access.get( command );
            Objects.requireNonNull(a);
            return a;
        }
    }

The Objects.requireNoNull() call is an assertion that is new to Java 8.

UserSecurity has a getAccess() method that will determine the access control (SHOW, HIDE, DISABLE) that will be applied to a given command.

Extra Init

I'm going to use a method "applySecurity(Scene)" which applies the access control.  This will be applied for each change in the UserSecurity ChoiceBox.  I also need to allow for the first pass.  Because I need the Scene, I can't make this call in the initialize() method because the Scene is not available.  Instead, I added an init() method to the MainViewController class.

public void init() {
        applySecurity(cbUser.getScene());
    }

And I call the update once the Stage is onShown.  This is code that appears in the Application subclass' start() method.  It is in a file called AccessControlMain which includes the public static void main.  (Unlike the rest of the code presented, this is not in the controller.)

    FXMLLoader fxmlLoader = new FXMLLoader(AccessControlMain.class.getResource("/fxml/MainView.fxml"));

    Parent p = fxmlLoader.load();
    
    MainViewController mainView = fxmlLoader.getController();

    Scene scene = new Scene(p);

    primaryStage.setTitle("Access Control");
    primaryStage.setScene(scene);
    primaryStage.setOnShown( (evt) ->  mainView.init() );
    primaryStage.show();

Recursion

The method applySecurity(Scene) is called in the post-initialize() method "init".  It's also called with each change in the UserSecurity ChoiceBox.  See the demo video.

The algorithm for applySecurity() is to check a node for the presence of a CommandType marker.  If one is found, an AccessType object is returned.  This will be used to set the following properties on the Node.

  1. SHOW.  Visible, managed, not disabled
  2. HIDE. Not visible, not managed, disabled
  3. DISABLE. Visible, managed, disabled

Visible and disabled should be familiar properties.  Managed is the technique I'm using to reclaim the space left over from a setVisible(false) call.  I set a Node to be un-managed (setManaged(false)), then follow up with a layout() command on the Parent.

First, the applySecurity() method which starts the recursion.

    private void applySecurity(Scene s) {
        applySecurity(s, cbUser.getSelectionModel().getSelectedItem(), s.getRoot());
    }

Next, an overridden function will process a Node.  If the Node is also a Parent, then iteration takes place (for children) with a recursive call made for each child.  This allows me to traverse the Node hierarchy.

There is a special section for MenuBars.  Because neither a Menu nor a MenuItem is a Parent, I can't use the same calls.  So, a special menu-specific version of applySecurity() will be shown later.

    private void applySecurity(Scene s, UserSecurity security, Node n) {

        if( n == null ) return;

        if( n.getProperties().containsKey(NodePropertiesKeyType.commandType) ) {

            //
            // This is a Node that should have security applied
            //
            CommandType command = (CommandType) n.getProperties().get(NodePropertiesKeyType.commandType );
            AccessType access = security.getAccess(command);

            switch( security.getAccess(command) ) {

                case SHOW:
                    n.setVisible(true);
                    n.setDisable(false);
                    n.setManaged(true);
                    break;
                case HIDE:
                    n.setVisible(false);
                    n.setDisable(true);
                    n.setManaged(false);
                    break;
                case DISABLE:
                    n.setVisible(true);
                    n.setDisable(true);
                    n.setManaged(true);
                    break;
            }
        }

        //
        // Menus and MenuItems are not Nodes
        //
        if( n instanceof MenuBar ) {
            MenuBar mb = (MenuBar)n;
            for( Menu toplevel : mb.getMenus() ) {
                applySecurity( s, security, toplevel );
            }
        }

        if( n instanceof Parent) {
            Parent p = (Parent)n;
            for( Node childNode : p.getChildrenUnmodifiable() ) {
                applySecurity( s, security, childNode );
            }
            p.layout();
        }
    }

This is the special menu version of the method.

   private void applySecurity(Scene s, UserSecurity security, MenuItem mi) {

        if( mi == null ) return;

        if( mi.getProperties().containsKey(NodePropertiesKeyType.commandType) ) {

            //
            // This is a Node that should have security applied
            //
            CommandType command = (CommandType) mi.getProperties().get(NodePropertiesKeyType.commandType );
            AccessType access = security.getAccess(command);

            switch( security.getAccess(command) ) {

                case SHOW:
                    mi.setVisible(true);
                    mi.setDisable(false);
                    break;
                case HIDE:
                    mi.setVisible(false);
                    mi.setDisable(true);
                    break;
                case DISABLE:
                    mi.setVisible(true);
                    mi.setDisable(true);
                    break;
            }
        }

        if( mi instanceof Menu ) {
            Menu m = (Menu)mi;
            for( MenuItem childMI : m.getItems() ) {
                applySecurity( s, security, childMI );
            }
        }
    }

This example marked Nodes that need to be secured with a CommandType property.  As access control changed, the UI could be reconfigured quickly by a scan of the Scene object graph.  All this was done using a UI in Scene Builder, rather than adding and deleting controls on-the-fly.  Also, the CSS is left untouched.  I haven't loaded the presentation with a bunch of non-functional security requirements.

All source code for this post is available on GitHub.




No comments:

Post a Comment